Skip to content

Commit

Permalink
Implementing a Database API for Python Admin SDK (#31)
Browse files Browse the repository at this point in the history
* Implemented get_token() method for App

* Adding db client

* Fleshed out the DatabaseReference type

* Implemented the full DB API

* Added unit tests for DB queries

* Support for service cleanup; More tests

* Further API cleanup

* Code cleanup and more test cases

* Fixing test for Python 3

* More python3 fixes

* Get/set priority

* Implementing query filters

* Implemented query filters

* Implemented a Query abstraction

* Adding integration tests for DB API

* Adding license headers to new files; Using the same testutils from unit tests

* Added integration tests for comlpex queries

* More integration tests for complex queries

* Some documentation and tests

* Improved error handling; More integration tests

* Updated API docs

* Implement support for sorting query results

* Support for sorting lists

* Fixed the sorting implementation and updated test cases

* Braking index ties by comparing their keys

* Updated integration tests to check for result order

* Updated docstrings

* Added newlines at the end of test data files

* Updated documentation; Fixed a bug in service cleanup logic; Other minor nits.
  • Loading branch information
hiranya911 committed Jun 1, 2017
1 parent f7240de commit 2246418
Show file tree
Hide file tree
Showing 12 changed files with 1,770 additions and 22 deletions.
77 changes: 75 additions & 2 deletions firebase_admin/__init__.py
Expand Up @@ -13,6 +13,7 @@
# limitations under the License.

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

import six
Expand All @@ -26,8 +27,10 @@

_apps = {}
_apps_lock = threading.RLock()
_clock = datetime.datetime.utcnow

_DEFAULT_APP_NAME = '[DEFAULT]'
_CLOCK_SKEW_SECONDS = 300


def initialize_app(credential=None, options=None, name=_DEFAULT_APP_NAME):
Expand Down Expand Up @@ -91,6 +94,7 @@ def delete_app(app):
with _apps_lock:
if _apps.get(app.name) is app:
del _apps[app.name]
app._cleanup() # pylint: disable=protected-access
return
if app.name == _DEFAULT_APP_NAME:
raise ValueError(
Expand Down Expand Up @@ -145,12 +149,16 @@ def __init__(self, options):
'must be a dictionary.'.format(type(options)))
self._options = options

def get(self, key):
"""Returns the option identified by the provided key."""
return self._options.get(key)


class App(object):
"""The entry point for Firebase Python SDK.
Represents a Firebase app, while holding the configuration and state
common to all Firebase APIs.
Represents a Firebase app, while holding the configuration and state
common to all Firebase APIs.
"""

def __init__(self, name, credential, options):
Expand All @@ -174,6 +182,9 @@ def __init__(self, name, credential, options):
'with a valid credential instance.')
self._credential = credential
self._options = _AppOptions(options)
self._token = None
self._lock = threading.RLock()
self._services = {}

@property
def name(self):
Expand All @@ -186,3 +197,65 @@ def credential(self):
@property
def options(self):
return self._options

def get_token(self):
"""Returns an OAuth2 bearer token.
This method may return a cached token. But it handles cache invalidation, and therefore
is guaranteed to always return unexpired tokens.
Returns:
string: An unexpired OAuth2 token.
"""
if not self._token_valid():
self._token = self._credential.get_access_token()
return self._token.access_token

def _token_valid(self):
if self._token is None:
return False
skewed_expiry = self._token.expiry - datetime.timedelta(seconds=_CLOCK_SKEW_SECONDS)
return _clock() < skewed_expiry

def _get_service(self, name, initializer):
"""Returns the service instance identified by the given name.
Services are functional entities exposed by the Admin SDK (e.g. auth, database). Each
service instance is associated with exactly one App. If the named service
instance does not exist yet, _get_service() calls the provided initializer function to
create the service instance. The created instance will be cached, so that subsequent
calls would always fetch it from the cache.
Args:
name: Name of the service to retrieve.
initializer: A function that can be used to initialize a service for the first time.
Returns:
object: The specified service instance.
Raises:
ValueError: If the provided name is invalid, or if the App is already deleted.
"""
if not name or not isinstance(name, six.string_types):
raise ValueError(
'Illegal name argument: "{0}". Name must be a non-empty string.'.format(name))
with self._lock:
if self._services is None:
raise ValueError(
'Service requested from deleted Firebase App: "{0}".'.format(self._name))
if name not in self._services:
self._services[name] = initializer(self)
return self._services[name]

def _cleanup(self):
"""Cleans up any services associated with this App.
Checks whether each service contains a close() method, and calls it if available.
This is to be called when an App is being deleted, thus ensuring graceful termination of
any services started by the App.
"""
with self._lock:
for service in self._services.values():
if hasattr(service, 'close') and hasattr(service.close, '__call__'):
service.close()
self._services = None
22 changes: 2 additions & 20 deletions firebase_admin/auth.py
Expand Up @@ -27,8 +27,8 @@
import google.oauth2.id_token
import six

import firebase_admin
from firebase_admin import credentials
from firebase_admin import utils

_auth_lock = threading.Lock()

Expand All @@ -39,20 +39,6 @@
GCLOUD_PROJECT_ENV_VAR = 'GCLOUD_PROJECT'


def _get_initialized_app(app):
if app is None:
return firebase_admin.get_app()
elif isinstance(app, firebase_admin.App):
initialized_app = firebase_admin.get_app(app.name)
if app is not initialized_app:
raise ValueError('Illegal app argument. App instance not '
'initialized via the firebase module.')
return app
else:
raise ValueError('Illegal app argument. Argument must be of type '
' firebase_admin.App, but given "{0}".'.format(type(app)))


def _get_token_generator(app):
"""Returns a _TokenGenerator instance for an App.
Expand All @@ -69,11 +55,7 @@ def _get_token_generator(app):
Raises:
ValueError: If the app argument is invalid.
"""
app = _get_initialized_app(app)
with _auth_lock:
if not hasattr(app, _AUTH_ATTRIBUTE):
setattr(app, _AUTH_ATTRIBUTE, _TokenGenerator(app))
return getattr(app, _AUTH_ATTRIBUTE)
return utils.get_app_service(app, _AUTH_ATTRIBUTE, _TokenGenerator)


def create_custom_token(uid, developer_claims=None, app=None):
Expand Down

0 comments on commit 2246418

Please sign in to comment.