8000 Multi DB support (#178) · fossabot/firebase-admin-python@3ff6a07 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3ff6a07

Browse files
authored
Multi DB support (firebase#178)
1 parent c420b4f commit 3ff6a07

File tree

3 files changed

+123
-63
lines changed

3 files changed

+123
-63
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Unreleased
22

3-
-
3+
- [added] The `db.reference()` method now optionally takes a `url`
4+
parameter. This can be used to access multiple Firebase Databases
5+
in the same project more easily.
46

57
# v2.12.0
68

firebase_admin/db.py

Lines changed: 68 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,28 @@
4141
_TRANSACTION_MAX_RETRIES = 25
4242

4343

44-
def reference(path='/', app=None):
44+
def reference(path='/', app=None, url=None):
4545
"""Returns a database Reference representing the node at the specified path.
4646
4747
If no path is specified, this function returns a Reference that represents the database root.
48+
By default, the returned References provide access to the Firebase Database specified at
49+
app initialization. To connect to a different Database instance in the same Firebase project,
50+
specify the ``url`` parameter.
4851
4952
Args:
5053
path: Path to a node in the Firebase realtime database (optional).
5154
app: An App instance (optional).
55+
url: Base URL of the Firebase Database instance (optional). When specified, takes
56+
precedence over the the ``databaseURL`` option set at app initialization.
5257
5358
Returns:
5459
Reference: A newly initialized Reference.
5560
5661
Raises:
5762
ValueError: If the specified path or app is invalid.
5863
"""
59-
client = _utils.get_app_service(app, _DB_ATTRIBUTE, _Client.from_app)
64+
service = _utils.get_app_service(app, _DB_ATTRIBUTE, _DatabaseService)
65+
client = service.get_client(url)
6066
return Reference(client=client, path=path)
6167

6268
def _parse_path(path):
@@ -662,65 +668,50 @@ def __eq__(self, other):
662668
return self._compare(other) is 0
663669

664670

665-
class _Client(_http_client.JsonHttpClient):
666-
"""HTTP client used to make REST calls.
667-
668-
_Client maintains an HTTP session, and handles authenticating HTTP requests along with
669-
marshalling and unmarshalling of JSON data.
670-
"""
671+
class _DatabaseService(object):
672+
"""Service that maintains a collection of database clients."""
671673

672674
_DEFAULT_AUTH_OVERRIDE = '_admin_'
673675

674-
def __init__(self, credential, base_url, auth_override=_DEFAULT_AUTH_OVERRIDE, timeout=None):
675-
"""Creates a new _Client from the given parameters.
676-
677-
This exists primarily to enable testing. For regular use, obtain _Client instances by
678-
calling the from_app() class method.
679-
680-
Args:
681-
credential: A Google credential that can be used to authenticate requests.
682-
base_url: A URL prefix to be added to all outgoing requests. This is typically the
683-
Firebase Realtime Database URL.
684-
auth_override: A dictionary representing auth variable overrides or None (optional).
685-
Default value provides admin privileges. A None value here provides un-authenticated
686-
guest privileges.
687-
timeout: HTTP request timeout in seconds (optional). If not set connections will never
688-
timeout, which is the default behavior of the underlying requests library.
689-
"""
690-
_http_client.JsonHttpClient.__init__(
691-
self, credential=credential, base_url=base_url, headers={'User-Agent': _USER_AGENT})
676+
def __init__(self, app):
677+
self._credential = app.credential.get_credential()
678+
db_url = app.options.get('databaseURL')
679+
if db_url:
680+
self._db_url = _DatabaseService._validate_url(db_url)
681+
else:
682+
self._db_url = None
683+
auth_override = _DatabaseService._get_auth_override(app)
692684
if auth_override != self._DEFAULT_AUTH_OVERRIDE and auth_override != {}:
693685
encoded = json.dumps(auth_override, separators=(',', ':'))
694686
self._auth_override = 'auth_variable_override={0}'.format(encoded)
695687
else:
696688
self._auth_override = None
697-
self._timeout = timeout
689+
self._timeout = app.options.get('httpTimeout')
690+
self._clients = {}
691+
692+
def get_client(self, base_url=None):
693+
if base_url is None:
694+
base_url = self._db_url
695+
base_url = _DatabaseService._validate_url(base_url)
696+
if base_url not in self._clients:
697+
client = _Client(self._credential, base_url, self._auth_override, self._timeout)
698+
self._clients[base_url] = client
699+
return self._clients[base_url]
698700

699701
@classmethod
700-
def from_app(cls, app):
701-
"""Creates a new _Client for a given App"""
702-
credential = app.credential.get_credential()
703-
db_url = cls._get_db_url(app)
704-
auth_override = cls._get_auth_override(app)
705-
timeout = app.options.get('httpTimeout')
706-
return _Client(credential, db_url, auth_override, timeout)
707-
708-
@classmethod
709-
def _get_db_url(cls, app):
710-
"""Retrieves and parses the database URL option."""
711-
url = app.options.get('databaseURL')
702+
def _validate_url(cls, url):
703+
"""Parses and validates a given database URL."""
712704
if not url or not isinstance(url, six.string_types):
713705
raise ValueError(
714-
'Invalid databaseURL option: "{0}". databaseURL must be a non-empty URL '
715-
'string.'.format(url))
716-
706+
'Invalid database URL: "{0}". Database URL must be a non-empty '
707+
'URL string.'.format(url))
717708
parsed = urllib.parse.urlparse(url)
718709
if parsed.scheme != 'https':
719710
raise ValueError(
720-
'Invalid databaseURL option: "{0}". databaseURL must be an HTTPS URL.'.format(url))
711+
'Invalid database URL: "{0}". Database URL must be an HTTPS URL.'.format(url))
721712
elif not parsed.netloc.endswith('.firebaseio.com'):
722713
raise ValueError(
723-
'Invalid databaseURL option: "{0}". databaseURL must be a valid URL to a '
714+
'Invalid database URL: "{0}". Database URL must be a valid URL to a '
724715
'Firebase Realtime Database instance.'.format(url))
725716
return 'https://{0}'.format(parsed.netloc)
726717

@@ -735,6 +726,39 @@ def _get_auth_override(cls, app):
735726
else:
736727
return auth_override
737728

729+
def close(self):
730+
for value in self._clients.values():
731+
value.close()
732+
self._clients = {}
733+
734+
735+
class _Client(_http_client.JsonHttpClient):
736+
"""HTTP client used to make REST calls.
737+
738+
_Client maintains an HTTP session, and handles authenticating HTTP requests along with
739+
marshalling and unmarshalling of JSON data.
740+
"""
741+
742+
def __init__(self, credential, base_url, auth_override, timeout):
743+
"""Creates a new _Client from the given parameters.
744+
745+
This exists primarily to enable testing. For regular use, obtain _Client instances by
746+
calling the from_app() class method.
747+
748+
Args:
749+
credential: A Google credential that can be used to authenticate requests.
750+
base_url: A URL prefix to be added to all outgoing requests. This is typically the
751+
Firebase Realtime Database URL.
752+
auth_override: The encoded auth_variable_override query parameter to be included in
753+
outgoing requests.
754+
timeout: HTTP request timeout in seconds. If not set connections will never
755+
timeout, which is the default behavior of the underlying requests library.
756+
"""
757+
_http_client.JsonHttpClient.__init__(
758+
self, credential=credential, base_url=base_url, headers={'User-Agent': _USER_AGENT})
759+
self._auth_override = auth_override
760+
self._timeout = timeout
761+
738762
@property
739763
def auth_override(self):
740764
return self._auth_override

tests/test_db.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -562,21 +562,45 @@ def test_invalid_db_url(self, url):
562562
firebase_admin.initialize_app(testutils.MockCredential(), {'databaseURL' : url})
563563
with pytest.raises(ValueError):
564564
db.reference()
565+
other_app = firebase_admin.initialize_app(testutils.MockCredential(), name='otherApp')
566+
with pytest.raises(ValueError):
567+
db.reference(app=other_app, url=url)
568+
569+
def test_multi_db_support(self):
570+
default_url = 'https://test.firebaseio.com'
571+
firebase_admin.initialize_app(testutils.MockCredential(), {
572+
'databaseURL' : default_url,
573+
})
574+
ref = db.reference()
575+
assert ref._client.base_url == default_url
576+
assert ref._client.auth_override is None
577+
assert ref._client.timeout is None
578+
assert ref._client is db.reference()._client
579+
assert ref._client is db.reference(url=default_url)._client
580+
581+
other_url = 'https://other.firebaseio.com'
582+
other_ref = db.reference(url=other_url)
583+
assert other_ref._client.base_url == other_url
584+
assert other_ref._client.auth_override is None
585+
assert other_ref._client.timeout is None
586+
assert other_ref._client is db.reference(url=other_url)._client
587+
assert other_ref._client is db.reference(url=other_url + '/')._client
565588

566589
@pytest.mark.parametrize('override', [{}, {'uid':'user1'}, None])
567590
def test_valid_auth_override(self, override):
568591
firebase_admin.initialize_app(testutils.MockCredential(), {
569592
'databaseURL' : 'https://test.firebaseio.com',
570593
'databaseAuthVariableOverride': override
571594
})
572-
ref = db.reference()
573-
assert ref._client.base_url == 'https://test.firebaseio.com'
574-
assert ref._client.timeout is None
575-
if override == {}:
576-
assert ref._client.auth_override is None
577-
else:
578-
encoded = json.dumps(override, separators=(',', ':'))
579-
assert ref._client.auth_override == 'auth_variable_override={0}'.format(encoded)
595+
default_ref = db.reference()
596+
other_ref = db.reference(url='https://other.firebaseio.com')
597+
for ref in [default_ref, other_ref]:
598+
assert ref._client.timeout is None
599+
if override == {}:
600+
assert ref._client.auth_override is None
601+
else:
602+
encoded = json.dumps(override, separators=(',', ':'))
603+
assert ref._client.auth_override == 'auth_variable_override={0}'.format(encoded)
580604

581605
@pytest.mark.parametrize('override', [
582606
'', 'foo', 0, 1, True, False, list(), tuple(), _Object()])
@@ -587,33 +611,43 @@ def test_invalid_auth_override(self, override):
587611
})
588612
with pytest.raises(ValueError):
589613
db.reference()
614+
other_app = firebase_admin.initialize_app(testutils.MockCredential(), {
615+
'databaseAuthVariableOverride': override
616+
}, name='otherApp')
617+
with pytest.raises(ValueError):
618+
db.reference(app=other_app, url='https://other.firebaseio.com')
590619

591620
def test_http_timeout(self):
592621
test_url = 'https://test.firebaseio.com'
593622
firebase_admin.initialize_app(testutils.MockCredential(), {
594623
'databaseURL' : test_url,
595624
'httpTimeout': 60
596625
})
597-
ref = db.reference()
598-
recorder = []
599-
adapter = MockAdapter('{}', 200, recorder)
600-
ref._client.session.mount(test_url, adapter)
601-
assert ref._client.base_url == test_url
602-
assert ref._client.timeout == 60
603-
assert ref.get() == {}
604-
assert len(recorder) == 1
605-
assert recorder[0]._extra_kwargs['timeout'] == 60
626+
default_ref = db.reference()
627+
other_ref = db.reference(url='https://other.firebaseio.com')
628+
for ref in [default_ref, other_ref]:
629+
recorder = []
630+
adapter = MockAdapter('{}', 200, recorder)
631+
ref._client.session.mount(ref._client.base_url, adapter)
632+
assert ref._client.timeout == 60
633+
assert ref.get() == {}
634+
assert len(recorder) == 1
635+
assert recorder[0]._extra_kwargs['timeout'] == 60
606636

607637
def test_app_delete(self):
608638
app = firebase_admin.initialize_app(
609639
testutils.MockCredential(), {'databaseURL' : 'https://test.firebaseio.com'})
610640
ref = db.reference()
611-
assert ref is not None
641+
other_ref = db.reference(url='https://other.firebaseio.com')
612642
assert ref._client.session is not None
643+
assert other_ref._client.session is not None
613644
firebase_admin.delete_app(app)
614645
with pytest.raises(ValueError):
615646
db.reference()
647+
with pytest.raises(ValueError):
648+
db.reference(url='https://other.firebaseio.com')
616649
assert ref._client.session is None
650+
assert other_ref._client.session is None
617651

618652
def test_user_agent_format(self):
619653
expected = 'Firebase/HTTP/{0}/{1}.{2}/AdminPython'.format(

0 commit comments

Comments
 (0)
0