10000 Auto init by avishalom · Pull Request #105 · firebase/firebase-admin-python · GitHub
[go: up one dir, main page]

Skip to content

Auto init #105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Jan 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
353aa0d
auto init
avishalom Dec 18, 2017
2c6f3ad
pretty the code
avishalom Dec 19, 2017
f06d5e1
initial
avishalom Dec 20, 2017
f05ee27
start
avishalom Dec 21, 2017
caedea9
Merge branch 'master' into auto_init
avishalom Dec 21, 2017
fb63e16
test bad json files, and add the fixtures for the good ones
avishalom Dec 21, 2017
4159bbc
add json files
avishalom Dec 21, 2017
657e0bd
test good json files for default config
avishalom Dec 21, 2017
3432c34
CREDENTIAL
avishalom Dec 21, 2017
1c524ec
CREDENTIAL
avishalom Dec 21, 2017
fb920fc
ignore the existing FIREBASE_CONFIG value in tests
avishalom Dec 23, 2017
a07b216
Merge branch 'master' into auto_init
avishalom Dec 23, 2017
9eb492a
lint
avishalom Dec 23, 2017
0f44837
Revert the update behavior of the FIREBASE_CONFIG env var. only use …
avishalom Dec 31, 2017
44c1efa
cleanup
avishalom Dec 31, 2017
284f7d7
lint
avishalom Dec 31, 2017
417e062
change fixture format and add json ability to env file
avishalom Jan 2, 2018
c3c54be
added tests for json env var, and rearanged named tests
avishalom Jan 3, 2018
e20592d
unify the open and read in one try block
avishalom Jan 3, 2018
58caa4b
PR comments, changing the parametrized fixture to a parametrized test…
avishalom Jan 6, 2018
140461c
add CREDENTIAL back (for unit tests without ADC set up)
avishalom Jan 6, 2018
9b59bef
add CREDENTIAL back (for unit tests without ADC set up)
avishalom Jan 6, 2018
2bc327d
move teardown code to finalizer in indirect fixture, to ensure teardo…
avishalom Jan 8, 2018
ade3983
renaming test cases for config options
avishalom Jan 8, 2018
b07314d
added test for bad json string
avishalom Jan 8, 2018
10747d4
whitespace
avishalom Jan 8, 2018
2837e41
loading options returns a dict an is non mutating
avishalom Jan 8, 2018
12929d0
cleanup
avishalom Jan 8, 2018
48650dd
line length lint (config option test names)
avishalom Jan 8, 2018
d85801b
Addressing comments. (cleanup code, and compare dictionaries directly.)
avishalom Jan 8, 2018
3b69a8b
initialize_app comment explaining getting options from env.
avishalom Jan 10, 2018
a4002aa
Merge branch 'master' into auto_init
avishalom Jan 10, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions firebase_admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""Firebase Admin SDK for Python."""
import datetime
import json
import os
import threading

Expand All @@ -31,7 +32,9 @@
_clock = datetime.datetime.utcnow

_DEFAULT_APP_NAME = '[DEFAULT]'

_FIREBASE_CONFIG_ENV_VAR = 'FIREBASE_CONFIG'
_CONFIG_VALID_KEYS = ['databaseAuthVariableOverride', 'databaseURL', 'httpTimeout', 'projectId',
'storageBucket']

def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
"""Initializes and returns a new App instance.
Expand All @@ -46,10 +49,14 @@ def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
credential: A credential object used to initialize the SDK (optional). If none is provided,
Google Application Default Credentials are used.
options: A dictionary of configuration options (optional). Supported options include
``databaseURL``, ``storageBucket`` and ``httpTimeout``. If ``httpTimeout`` is not set,
HTTP connections initiated by client modules such as ``db`` will not time out.
``databaseURL``, ``storageBucket``, ``projectId``, ``databaseAuthVariableOverride``
and ``httpTimeout``. If ``httpTimeout`` is not set, HTTP connections initiated by client
modules such as ``db`` will not time out.
If options are not provided an attempt is made to load the options from the environment.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be part of the method description, rather than the argument description.

This is done by looking up the ``FIREBASE_CONFIG`` environment variable. If the value of
the variable starts with ``"{"``, it is parsed as a JSON object. Otherwise it is treated
as a file name and the JSON content is read from the corresponding file.
name: Name of the app (optional).

Returns:
App: A newly initialized instance of App.

Expand Down Expand Up @@ -145,7 +152,8 @@ class _AppOptions(object):

def __init__(self, options):
if options is None:
options = {}
options = self._load_from_environment()

if not isinstance(options, dict):
raise ValueError('Illegal Firebase app options type: {0}. Options '
'must be a dictionary.'.format(type(options)))
Expand All @@ -155,6 +163,30 @@ def get(self, key, default=None):
"""Returns the option identified by the provided key."""
return self._options.get(key, default)

def _load_from_environment(self):
"""Invoked when no options are passed to __init__, loads options from FIREBASE_CONFIG.

If the value of the FIREBASE_CONFIG environment variable starts with "{" an attempt is made
to parse it as a JSON object, otherwise it is assumed to be pointing to a JSON file.
"""

config_file = os.getenv(_FIREBASE_CONFIG_ENV_VAR)
if not config_file:
return {}
if config_file.startswith('{'):
json_str = config_file
else:
try:
with open(config_file, 'r') as json_file:
json_str = json_file.read()
except Exception as err:
raise ValueError('Unable to read file {}. {}'.format(config_file, err))
try:
json_data = json.loads(json_str)
except Exception as err:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we not handle this exception, and just let it be thrown?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can,
the error message is "Expecting property name: line 1 column 2 (char 1)"
I think it is clearer if we add information, but willing to remove if you want it gone.

raise ValueError('JSON string "{0}" is not valid json. {1}'.format(json_str, err))
return {k: v for k, v in json_data.items() if k in _CONFIG_VALID_KEYS}


class App(object):
"""The entry point for Firebase Python SDK.
Expand Down
6 changes: 6 additions & 0 deletions tests/data/firebase_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"databaseAuthVariableOverride": {"some_key": "some_val"},
"databaseURL": "https://hipster-chat.firebaseio.mock",
"projectId": "hipster-chat-mock",
"storageBucket": "hipster-chat.appspot.mock"
}
Empty file.
D7AE 1 change: 1 addition & 0 deletions tests/data/firebase_config_invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
baaaaad
4 changes: 4 additions & 0 deletions tests/data/firebase_config_invalid_key.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"databaseUrrrrL": "https://hipster-chat.firebaseio.mock",
"projectId": "hipster-chat-mock"
}
4 changes: 4 additions & 0 deletions tests/data/firebase_config_partial.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"databaseURL": "https://hipster-chat.firebaseio.mock",
"projectId": "hipster-chat-mock"
}
149 changes: 147 additions & 2 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

"""Tests for firebase_admin.App."""
from collections import namedtuple
import os

import pytest
Expand All @@ -22,10 +23,13 @@
from firebase_admin import _utils
from tests import testutils


CREDENTIAL = credentials.Certificate(
testutils.resource_filename('service_account.json'))
GCLOUD_PROJECT = 'GCLOUD_PROJECT'
CONFIG_JSON = firebase_admin._FIREBASE_CONFIG_ENV_VAR

# This fixture will ignore the environment variable pointing to the default
# configuration for the duration of the tests.

class CredentialProvider(object):
def init(self):
Expand Down Expand Up @@ -74,7 +78,6 @@ class AppService(object):
def __init__(self, app):
self._app = app


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

@pytest.fixture(scope="function")
def env_test_case(request):
config_old = set_config_env(request.param.config_json)
yield request.param
revert_config_env(config_old)


EnvOptionsTestCase = namedtuple('EnvOptionsTestCase',
'name, config_json, init_options, want_options')
env_options_test_cases = [
EnvOptionsTestCase(name='Environment var not set, initialized with an empty options dict',
config_json=None,
init_options={},
want_options={}),
EnvOptionsTestCase(name='Environment var empty, initialized with an empty options dict',
config_json='',
init_options={},
want_options={}),
EnvOptionsTestCase(name='Environment var not set, initialized with no options dict',
config_json=None,
init_options=None,
want_options={}),
EnvOptionsTestCase(name='Environment empty, initialized with no options dict',
config_json='',
init_options=None,
want_options={}),
EnvOptionsTestCase(name='Environment var not set, initialized with options dict',
config_json=None,
init_options={'storageBucket': 'bucket1'},
want_options={'storageBucket': 'bucket1'}),
EnvOptionsTestCase(name='Environment var set to file but ignored, initialized with options',
config_json='firebase_config.json',
init_options={'storageBucket': 'bucket1'},
want_options={'storageBucket': 'bucket1'}),
EnvOptionsTestCase(name='Environment var set to json but ignored, initialized with options',
config_json='{"storageBucket": "hipster-chat.appspot.mock"}',
init_options={'storageBucket': 'bucket1'},
want_options={'storageBucket': 'bucket1'}),
EnvOptionsTestCase(name='Environment var set to file, initialized with no options dict',
config_json='firebase_config.json',
init_options=None,
want_options={'databaseAuthVariableOverride': {'some_key': 'some_val'},
'databaseURL': 'https://hipster-chat.firebaseio.mock',
'projectId': 'hipster-chat-mock',
'storageBucket': 'hipster-chat.appspot.mock'}),
EnvOptionsTestCase(name='Environment var set to json string, initialized with no options dict',
config_json='{"databaseAuthVariableOverride": {"some_key": "some_val"}, ' +
'"databaseURL": "https://hipster-chat.firebaseio.mock", ' +
'"projectId": "hipster-chat-mock",' +
'"storageBucket": "hipster-chat.appspot.mock"}',
init_options=None,
want_options={'databaseAuthVariableOverride': {'some_key': 'some_val'},
'databaseURL': 'https://hipster-chat.firebaseio.mock',
'projectId': 'hipster-chat-mock',
'storageBucket': 'hipster-chat.appspot.mock'}),
EnvOptionsTestCase(name='Invalid key in json file is ignored, the rest of the values are used',
config_json='firebase_config_invalid_key.json',
init_options=None,
want_options={'projectId': 'hipster-chat-mock'}),
EnvOptionsTestCase(name='Invalid key in json file is ignored, the rest of the values are used',
config_json='{"databaseUrrrrL": "https://hipster-chat.firebaseio.mock",' +
'"projectId": "hipster-chat-mock"}',
init_options=None,
want_options={'projectId': 'hipster-chat-mock'}),
EnvOptionsTestCase(name='Environment var set to file but ignored, init empty options dict',
config_json='firebase_config.json',
init_options={},
want_options={}),
EnvOptionsTestCase(name='Environment var set to string but ignored, init empty options dict',
config_json='{"projectId": "hipster-chat-mock"}',
init_options={},
want_options={}),
EnvOptionsTestCase(name='Environment variable set to json file with some options set',
config_json='firebase_config_partial.json',
init_options=None,
want_options={'databaseURL': 'https://hipster-chat.firebaseio.mock',
'projectId': 'hipster-chat-mock'}),
EnvOptionsTestCase(name='Environment variable set to json string with some options set',
config_json='{"databaseURL": "https://hipster-chat.firebaseio.mock",' +
'"projectId": "hipster-chat-mock"}',
init_options=None,
want_options={'databaseURL': 'https://hipster-chat.firebaseio.mock',
'projectId': 'hipster-chat-mock'}),
EnvOptionsTestCase(name='Environment var set to json file but ignored, init with options dict',
config_json='firebase_config_partial.json',
init_options={'projectId': 'pid1-mock',
'storageBucket': 'sb1-mock'},
want_options={'projectId': 'pid1-mock',
'storageBucket': 'sb1-mock'}),
EnvOptionsTestCase(name='Environment var set to file but ignored, init with full options dict',
config_json='firebase_config.json',
init_options={'databaseAuthVariableOverride': 'davy1-mock',
'databaseURL': 'https://db1-mock',
'projectId': 'pid1-mock',
'storageBucket': 'sb1-.mock'},
want_options={'databaseAuthVariableOverride': 'davy1-mock',
'databaseURL': 'https://db1-mock',
'projectId': 'pid1-mock',
'storageBucket': 'sb1-.mock'})]

def set_config_env(config_json):
config_old = os.environ.get(CONFIG_JSON)
if config_json is not None:
if not config_json or config_json.startswith('{'):
os.environ[CONFIG_JSON] = config_json
else:
os.environ[CONFIG_JSON] = testutils.resource_filename(
config_json)
elif os.environ.get(CONFIG_JSON) is not None:
del os.environ[CONFIG_JSON]
return config_old


def revert_config_env(config_old):
if config_old is not None:
os.environ[CONFIG_JSON] = config_old
elif os.environ.get(CONFIG_JSON) is not None:
del os.environ[CONFIG_JSON]

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


@pytest.mark.parametrize('bad_file_name', ['firebase_config_empty.json',
'firebase_config_invalid.json',
'no_such_file'])
def test_app_init_with_invalid_config_file(self, bad_file_name):
config_old = set_config_env(bad_file_name)
with pytest.raises(ValueError):
firebase_admin.initialize_app(CREDENTIAL)
revert_config_env(config_old)

def test_app_init_with_invalid_config_string(self):
config_old = set_config_env('{,,')
with pytest.raises(ValueError):
firebase_admin.initialize_app(CREDENTIAL)
revert_config_env(config_old)


@pytest.mark.parametrize('env_test_case', env_options_test_cases,
ids=[x.name for x in env_options_test_cases],
indirect=['env_test_case'])
def test_app_init_with_default_config(self, env_test_case):
app = firebase_admin.initialize_app(CREDENTIAL, options=env_test_case.init_options)
assert app.options._options == env_test_case.want_options

def test_project_id_from_options(self, app_credential):
app = firebase_admin.initialize_app(
app_credential, options={'projectId': 'test-project'}, name='myApp')
Expand Down
0