Skip to content

Commit f5d6071

Browse files
authored
Auto init (#105)
Read the default options from the FIREBASE_CONFIG env var if options are not passed to initialize_app.
1 parent cd4e7d6 commit f5d6071

7 files changed

+199
-7
lines changed

firebase_admin/__init__.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""Firebase Admin SDK for Python."""
1616
import datetime
17+
import json
1718
import os
1819
import threading
1920

@@ -31,7 +32,9 @@
3132
_clock = datetime.datetime.utcnow
3233

3334
_DEFAULT_APP_NAME = '[DEFAULT]'
34-
35+
_FIREBASE_CONFIG_ENV_VAR = 'FIREBASE_CONFIG'
36+
_CONFIG_VALID_KEYS = ['databaseAuthVariableOverride', 'databaseURL', 'httpTimeout', 'projectId',
37+
'storageBucket']
3538

3639
def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
3740
"""Initializes and returns a new App instance.
@@ -46,10 +49,14 @@ def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
4649
credential: A credential object used to initialize the SDK (optional). If none is provided,
4750
Google Application Default Credentials are used.
4851
options: A dictionary of configuration options (optional). Supported options include
49-
``databaseURL``, ``storageBucket`` and ``httpTimeout``. If ``httpTimeout`` is not set,
50-
HTTP connections initiated by client modules such as ``db`` will not time out.
52+
``databaseURL``, ``storageBucket``, ``projectId``, ``databaseAuthVariableOverride``
53+
and ``httpTimeout``. If ``httpTimeout`` is not set, HTTP connections initiated by client
54+
modules such as ``db`` will not time out.
55+
If options are not provided an attempt is made to load the options from the environment.
56+
This is done by looking up the ``FIREBASE_CONFIG`` environment variable. If the value of
57+
the variable starts with ``"{"``, it is parsed as a JSON object. Otherwise it is treated
58+
as a file name and the JSON content is read from the corresponding file.
5159
name: Name of the app (optional).
52-
5360
Returns:
5461
App: A newly initialized instance of App.
5562
@@ -145,7 +152,8 @@ class _AppOptions(object):
145152

146153
def __init__(self, options):
147154
if options is None:
148-
options = {}
155+
options = self._load_from_environment()
156+
149157
if not isinstance(options, dict):
150158
raise ValueError('Illegal Firebase app options type: {0}. Options '
151159
'must be a dictionary.'.format(type(options)))
@@ -155,6 +163,30 @@ def get(self, key, default=None):
155163
"""Returns the option identified by the provided key."""
156164
return self._options.get(key, default)
157165

166+
def _load_from_environment(self):
167+
"""Invoked when no options are passed to __init__, loads options from FIREBASE_CONFIG.
168+
169+
If the value of the FIREBASE_CONFIG environment variable starts with "{" an attempt is made
170+
to parse it as a JSON object, otherwise it is assumed to be pointing to a JSON file.
171+
"""
172+
173+
config_file = os.getenv(_FIREBASE_CONFIG_ENV_VAR)
174+
if not config_file:
175+
return {}
176+
if config_file.startswith('{'):
177+
json_str = config_file
178+
else:
179+
try:
180+
with open(config_file, 'r') as json_file:
181+
json_str = json_file.read()
182+
except Exception as err:
183+
raise ValueError('Unable to read file {}. {}'.format(config_file, err))
184+
try:
185+
json_data = json.loads(json_str)
186+
except Exception as err:
187+
raise ValueError('JSON string "{0}" is not valid json. {1}'.format(json_str, err))
188+
return {k: v for k, v in json_data.items() if k in _CONFIG_VALID_KEYS}
189+
158190

159191
class App(object):
160192
"""The entry point for Firebase Python SDK.

tests/data/firebase_config.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"databaseAuthVariableOverride": {"some_key": "some_val"},
3+
"databaseURL": "https://hipster-chat.firebaseio.mock",
4+
"projectId": "hipster-chat-mock",
5+
"storageBucket": "hipster-chat.appspot.mock"
6+
}

tests/data/firebase_config_empty.json

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
baaaaad
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"databaseUrrrrL": "https://hipster-chat.firebaseio.mock",
3+
"projectId": "hipster-chat-mock"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"databaseURL": "https://hipster-chat.firebaseio.mock",
3+
"projectId": "hipster-chat-mock"
4+
}

tests/test_app.py

Lines changed: 147 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
"""Tests for firebase_admin.App."""
16+
from collections import namedtuple
1617
import os
1718

1819
import pytest
@@ -22,10 +23,13 @@
2223
from firebase_admin import _utils
2324
from tests import testutils
2425

25-
2626
CREDENTIAL = credentials.Certificate(
2727
testutils.resource_filename('service_account.json'))
2828
GCLOUD_PROJECT = 'GCLOUD_PROJECT'
29+
CONFIG_JSON = firebase_admin._FIREBASE_CONFIG_ENV_VAR
30+
31+
# This fixture will ignore the environment variable pointing to the default
32+
# configuration for the duration of the tests.
2933

3034
class CredentialProvider(object):
3135
def init(self):
@@ -74,7 +78,6 @@ class AppService(object):
7478
def __init__(self, app):
7579
self._app = app
7680

77-
7881
@pytest.fixture(params=[Cert(), RefreshToken(), ExplicitAppDefault(), ImplicitAppDefault()],
7982
ids=['cert', 'refreshtoken', 'explicit-appdefault', 'implicit-appdefault'])
8083
def app_credential(request):
@@ -90,6 +93,124 @@ def init_app(request):
9093
else:
9194
return firebase_admin.initialize_app(CREDENTIAL)
9295

96+
@pytest.fixture(scope="function")
97+
def env_test_case(request):
98+
config_old = set_config_env(request.param.config_json)
99+
yield request.param
100+
revert_config_env(config_old)
101+
102+
103+
EnvOptionsTestCase = namedtuple('EnvOptionsTestCase',
104+
'name, config_json, init_options, want_options')
105+
env_options_test_cases = [
106+
EnvOptionsTestCase(name='Environment var not set, initialized with an empty options dict',
107+
config_json=None,
108+
init_options={},
109+
want_options={}),
110+
EnvOptionsTestCase(name='Environment var empty, initialized with an empty options dict',
111+
config_json='',
112+
init_options={},
113+
want_options={}),
114+
EnvOptionsTestCase(name='Environment var not set, initialized with no options dict',
115+
config_json=None,
116+
init_options=None,
117+
want_options={}),
118+
EnvOptionsTestCase(name='Environment empty, initialized with no options dict',
119+
config_json='',
120+
init_options=None,
121+
want_options={}),
122+
EnvOptionsTestCase(name='Environment var not set, initialized with options dict',
123+
config_json=None,
124+
init_options={'storageBucket': 'bucket1'},
125+
want_options={'storageBucket': 'bucket1'}),
126+
EnvOptionsTestCase(name='Environment var set to file but ignored, initialized with options',
127+
config_json='firebase_config.json',
128+
init_options={'storageBucket': 'bucket1'},
129+
want_options={'storageBucket': 'bucket1'}),
130+
EnvOptionsTestCase(name='Environment var set to json but ignored, initialized with options',
131+
config_json='{"storageBucket": "hipster-chat.appspot.mock"}',
132+
init_options={'storageBucket': 'bucket1'},
133+
want_options={'storageBucket': 'bucket1'}),
134+
EnvOptionsTestCase(name='Environment var set to file, initialized with no options dict',
135+
config_json='firebase_config.json',
136+
init_options=None,
137+
want_options={'databaseAuthVariableOverride': {'some_key': 'some_val'},
138+
'databaseURL': 'https://hipster-chat.firebaseio.mock',
139+
'projectId': 'hipster-chat-mock',
140+
'storageBucket': 'hipster-chat.appspot.mock'}),
141+
EnvOptionsTestCase(name='Environment var set to json string, initialized with no options dict',
142+
config_json='{"databaseAuthVariableOverride": {"some_key": "some_val"}, ' +
143+
'"databaseURL": "https://hipster-chat.firebaseio.mock", ' +
144+
'"projectId": "hipster-chat-mock",' +
145+
'"storageBucket": "hipster-chat.appspot.mock"}',
146+
init_options=None,
147+
want_options={'databaseAuthVariableOverride': {'some_key': 'some_val'},
148+
'databaseURL': 'https://hipster-chat.firebaseio.mock',
149+
'projectId': 'hipster-chat-mock',
150+
'storageBucket': 'hipster-chat.appspot.mock'}),
151+
EnvOptionsTestCase(name='Invalid key in json file is ignored, the rest of the values are used',
152+
config_json='firebase_config_invalid_key.json',
153+
init_options=None,
154+
want_options={'projectId': 'hipster-chat-mock'}),
155+
EnvOptionsTestCase(name='Invalid key in json file is ignored, the rest of the values are used',
156+
config_json='{"databaseUrrrrL": "https://hipster-chat.firebaseio.mock",' +
157+
'"projectId": "hipster-chat-mock"}',
158+
init_options=None,
159+
want_options={'projectId': 'hipster-chat-mock'}),
160+
EnvOptionsTestCase(name='Environment var set to file but ignored, init empty options dict',
161+
config_json='firebase_config.json',
162+
init_options={},
163+
want_options={}),
164+
EnvOptionsTestCase(name='Environment var set to string but ignored, init empty options dict',
165+
config_json='{"projectId": "hipster-chat-mock"}',
166+
init_options={},
167+
want_options={}),
168+
EnvOptionsTestCase(name='Environment variable set to json file with some options set',
169+
config_json='firebase_config_partial.json',
170+
init_options=None,
171+
want_options={'databaseURL': 'https://hipster-chat.firebaseio.mock',
172+
'projectId': 'hipster-chat-mock'}),
173+
EnvOptionsTestCase(name='Environment variable set to json string with some options set',
174+
config_json='{"databaseURL": "https://hipster-chat.firebaseio.mock",' +
175+
'"projectId": "hipster-chat-mock"}',
176+
init_options=None,
177+
want_options={'databaseURL': 'https://hipster-chat.firebaseio.mock',
178+
'projectId': 'hipster-chat-mock'}),
179+
EnvOptionsTestCase(name='Environment var set to json file but ignored, init with options dict',
180+
config_json='firebase_config_partial.json',
181+
init_options={'projectId': 'pid1-mock',
182+
'storageBucket': 'sb1-mock'},
183+
want_options={'projectId': 'pid1-mock',
184+
'storageBucket': 'sb1-mock'}),
185+
EnvOptionsTestCase(name='Environment var set to file but ignored, init with full options dict',
186+
config_json='firebase_config.json',
187+
init_options={'databaseAuthVariableOverride': 'davy1-mock',
188+
'databaseURL': 'https://db1-mock',
189+
'projectId': 'pid1-mock',
190+
'storageBucket': 'sb1-.mock'},
191+
want_options={'databaseAuthVariableOverride': 'davy1-mock',
192+
'databaseURL': 'https://db1-mock',
193+
'projectId': 'pid1-mock',
194+
'storageBucket': 'sb1-.mock'})]
195+
196+
def set_config_env(config_json):
197+
config_old = os.environ.get(CONFIG_JSON)
198+
if config_json is not None:
199+
if not config_json or config_json.startswith('{'):
200+
os.environ[CONFIG_JSON] = config_json
201+
else:
202+
os.environ[CONFIG_JSON] = testutils.resource_filename(
203+
config_json)
204+
elif os.environ.get(CONFIG_JSON) is not None:
205+
del os.environ[CONFIG_JSON]
206+
return config_old
207+
208+
209+
def revert_config_env(config_old):
210+
if config_old is not None:
211+
os.environ[CONFIG_JSON] = config_old
212+
elif os.environ.get(CONFIG_JSON) is not None:
213+
del os.environ[CONFIG_JSON]
93214

94215
class TestFirebaseApp(object):
95216
"""Test cases for App initialization and life cycle."""
@@ -140,6 +261,30 @@ def test_app_init_with_invalid_name(self, name):
140261
with pytest.raises(ValueError):
141262
firebase_admin.initialize_app(CREDENTIAL, name=name)
142263

264+
265+
@pytest.mark.parametrize('bad_file_name', ['firebase_config_empty.json',
266+
'firebase_config_invalid.json',
267+
'no_such_file'])
268+
def test_app_init_with_invalid_config_file(self, bad_file_name):
269+
config_old = set_config_env(bad_file_name)
270+
with pytest.raises(ValueError):
271+
firebase_admin.initialize_app(CREDENTIAL)
272+
revert_config_env(config_old)
273+
274+
def test_app_init_with_invalid_config_string(self):
275+
config_old = set_config_env('{,,')
276+
with pytest.raises(ValueError):
277+
firebase_admin.initialize_app(CREDENTIAL)
278+
revert_config_env(config_old)
279+
280+
281+
@pytest.mark.parametrize('env_test_case', env_options_test_cases,
282+
ids=[x.name for x in env_options_test_cases],
283+
indirect=['env_test_case'])
284+
def test_app_init_with_default_config(self, env_test_case):
285+
app = firebase_admin.initialize_app(CREDENTIAL, options=env_test_case.init_options)
286+
assert app.options._options == env_test_case.want_options
287+
143288
def test_project_id_from_options(self, app_credential):
144289
app = firebase_admin.initialize_app(
145290
app_credential, options={'projectId': 'test-project'}, name='myApp')

0 commit comments

Comments
 (0)