From 1536e33bdbd658fa4cf8c81a4b8fafbc2430f159 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <55252373+jkyle109@users.noreply.github.com> Date: Wed, 13 Jul 2022 14:15:23 -0400 Subject: [PATCH 1/3] feat(firestore): Expose Async Firestore Client. (#621) * feat(firestore): Expose Async Firestore Client. * fix: Added type hints and defintion wording changes * fix: removed future annotations until Python 3.6 is depreciated. * fix: added missed type and clarifying comment for Python 3.6 type hinting. * fix: lint --- firebase_admin/firestore_async.py | 82 +++++++++++++++++++++++++++++++ tests/test_firestore_async.py | 81 ++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 firebase_admin/firestore_async.py create mode 100644 tests/test_firestore_async.py diff --git a/firebase_admin/firestore_async.py b/firebase_admin/firestore_async.py new file mode 100644 index 000000000..a63d5a761 --- /dev/null +++ b/firebase_admin/firestore_async.py @@ -0,0 +1,82 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Cloud Firestore Async module. + +This module contains utilities for asynchronusly accessing the Google Cloud Firestore databases +associated with Firebase apps. This requires the ``google-cloud-firestore`` Python module. +""" + +from typing import Type + +from firebase_admin import ( + App, + _utils, +) +from firebase_admin.credentials import Base + +try: + from google.cloud import firestore # type: ignore # pylint: disable=import-error,no-name-in-module + existing = globals().keys() + for key, value in firestore.__dict__.items(): + if not key.startswith('_') and key not in existing: + globals()[key] = value +except ImportError: + raise ImportError('Failed to import the Cloud Firestore library for Python. Make sure ' + 'to install the "google-cloud-firestore" module.') + +_FIRESTORE_ASYNC_ATTRIBUTE: str = '_firestore_async' + + +def client(app: App = None) -> firestore.AsyncClient: + """Returns an async client that can be used to interact with Google Cloud Firestore. + + Args: + app: An App instance (optional). + + Returns: + google.cloud.firestore.Firestore_Async: A `Firestore Async Client`_. + + Raises: + ValueError: If a project ID is not specified either via options, credentials or + environment variables, or if the specified project ID is not a valid string. + + .. _Firestore Async Client: https://googleapis.dev/python/firestore/latest/client.html + """ + fs_client = _utils.get_app_service( + app, _FIRESTORE_ASYNC_ATTRIBUTE, _FirestoreAsyncClient.from_app) + return fs_client.get() + + +class _FirestoreAsyncClient: + """Holds a Google Cloud Firestore Async Client instance.""" + + def __init__(self, credentials: Type[Base], project: str) -> None: + self._client = firestore.AsyncClient(credentials=credentials, project=project) + + def get(self) -> firestore.AsyncClient: + return self._client + + @classmethod + def from_app(cls, app: App) -> "_FirestoreAsyncClient": + # Replace remove future reference quotes by importing annotations in Python 3.7+ b/238779406 + """Creates a new _FirestoreAsyncClient for the specified app.""" + credentials = app.credential.get_credential() + project = app.project_id + if not project: + raise ValueError( + 'Project ID is required to access Firestore. Either set the projectId option, ' + 'or use service account credentials. Alternatively, set the GOOGLE_CLOUD_PROJECT ' + 'environment variable.') + return _FirestoreAsyncClient(credentials, project) diff --git a/tests/test_firestore_async.py b/tests/test_firestore_async.py new file mode 100644 index 000000000..0fb17c813 --- /dev/null +++ b/tests/test_firestore_async.py @@ -0,0 +1,81 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for firebase_admin.firestore_async.""" + +import platform + +import pytest + +import firebase_admin +from firebase_admin import credentials +try: + from firebase_admin import firestore_async +except ImportError: + pass +from tests import testutils + + +@pytest.mark.skipif( + platform.python_implementation() == 'PyPy', + reason='Firestore is not supported on PyPy') +class TestFirestoreAsync: + """Test class Firestore Async APIs.""" + + def teardown_method(self, method): + del method + testutils.cleanup_apps() + + def test_no_project_id(self): + def evaluate(): + firebase_admin.initialize_app(testutils.MockCredential()) + with pytest.raises(ValueError): + firestore_async.client() + testutils.run_without_project_id(evaluate) + + def test_project_id(self): + cred = credentials.Certificate(testutils.resource_filename('service_account.json')) + firebase_admin.initialize_app(cred, {'projectId': 'explicit-project-id'}) + client = firestore_async.client() + assert client is not None + assert client.project == 'explicit-project-id' + + def test_project_id_with_explicit_app(self): + cred = credentials.Certificate(testutils.resource_filename('service_account.json')) + app = firebase_admin.initialize_app(cred, {'projectId': 'explicit-project-id'}) + client = firestore_async.client(app=app) + assert client is not None + assert client.project == 'explicit-project-id' + + def test_service_account(self): + cred = credentials.Certificate(testutils.resource_filename('service_account.json')) + firebase_admin.initialize_app(cred) + client = firestore_async.client() + assert client is not None + assert client.project == 'mock-project-id' + + def test_service_account_with_explicit_app(self): + cred = credentials.Certificate(testutils.resource_filename('service_account.json')) + app = firebase_admin.initialize_app(cred) + client = firestore_async.client(app=app) + assert client is not None + assert client.project == 'mock-project-id' + + def test_geo_point(self): + geo_point = firestore_async.GeoPoint(10, 20) # pylint: disable=no-member + assert geo_point.latitude == 10 + assert geo_point.longitude == 20 + + def test_server_timestamp(self): + assert firestore_async.SERVER_TIMESTAMP is not None # pylint: disable=no-member From eb8226b066da2b5eb367c7b0f50632b152374576 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <55252373+jkyle109@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:08:14 -0400 Subject: [PATCH 2/3] Adds integration tests for the Async Firstore module (#623) * Add integration tests for async firstore module * fix: made pytest Python 3.6 compatible * Trigger Integration Tests * fix: correct copyright year --- integration/conftest.py | 10 ++++++ integration/test_firestore_async.py | 53 +++++++++++++++++++++++++++++ requirements.txt | 1 + 3 files changed, 64 insertions(+) create mode 100644 integration/test_firestore_async.py diff --git a/integration/conftest.py b/integration/conftest.py index 169e02d5b..71f53f612 100644 --- a/integration/conftest.py +++ b/integration/conftest.py @@ -15,6 +15,7 @@ """pytest configuration and global fixtures for integration tests.""" import json +import asyncio import pytest import firebase_admin @@ -70,3 +71,12 @@ def api_key(request): 'command-line option.') with open(path) as keyfile: return keyfile.read().strip() + +@pytest.fixture(scope="session") +def event_loop(): + """Create an instance of the default event loop for test session. + This avoids early eventloop closure. + """ + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() diff --git a/integration/test_firestore_async.py b/integration/test_firestore_async.py new file mode 100644 index 000000000..2a5b93217 --- /dev/null +++ b/integration/test_firestore_async.py @@ -0,0 +1,53 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Integration tests for firebase_admin.firestore_async module.""" +import datetime +import pytest + +from firebase_admin import firestore_async + +@pytest.mark.asyncio +async def test_firestore_async(): + client = firestore_async.client() + expected = { + 'name': u'Mountain View', + 'country': u'USA', + 'population': 77846, + 'capital': False + } + doc = client.collection('cities').document() + await doc.set(expected) + + data = await doc.get() + assert data.to_dict() == expected + + await doc.delete() + data = await doc.get() + assert data.exists is False + +@pytest.mark.asyncio +async def test_server_timestamp(): + client = firestore_async.client() + expected = { + 'name': u'Mountain View', + 'timestamp': firestore_async.SERVER_TIMESTAMP # pylint: disable=no-member + } + doc = client.collection('cities').document() + await doc.set(expected) + + data = await doc.get() + data = data.to_dict() + assert isinstance(data['timestamp'], datetime.datetime) + await doc.delete() diff --git a/requirements.txt b/requirements.txt index 0dd529c04..87142fe93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ pylint == 2.3.1 pytest >= 6.2.0 pytest-cov >= 2.4.0 pytest-localserver >= 0.4.1 +pytest-asyncio >= 0.16.0 cachecontrol >= 0.12.6 google-api-core[grpc] >= 1.22.1, < 3.0.0dev; platform.python_implementation != 'PyPy' From ac697bc68d736040cb6a80c9efb1aeb878168fa4 Mon Sep 17 00:00:00 2001 From: Jonathan Edey <55252373+jkyle109@users.noreply.github.com> Date: Fri, 5 Aug 2022 10:24:44 -0400 Subject: [PATCH 3/3] Add code snippets for firestore modules. (#628) * Add code snippets for firestore modules. * fix: clarified snippet names and fixed newline. * fix: Removed var tags. These won't work as I intended it to since html is escaped when using includecode. --- snippets/firestore/__init__.py | 0 snippets/firestore/firestore.py | 84 ++++++++++++++++ snippets/firestore/firestore_async.py | 132 ++++++++++++++++++++++++++ 3 files changed, 216 insertions(+) create mode 100644 snippets/firestore/__init__.py create mode 100644 snippets/firestore/firestore.py create mode 100644 snippets/firestore/firestore_async.py diff --git a/snippets/firestore/__init__.py b/snippets/firestore/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/snippets/firestore/firestore.py b/snippets/firestore/firestore.py new file mode 100644 index 000000000..18040b742 --- /dev/null +++ b/snippets/firestore/firestore.py @@ -0,0 +1,84 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from firebase_admin import firestore + +# pylint: disable=invalid-name +def init_firestore_client(): + # [START init_firestore_client] + import firebase_admin + from firebase_admin import firestore + + # Application Default credentials are automatically created. + app = firebase_admin.initialize_app() + db = firestore.client() + # [END init_firestore_client] + +def init_firestore_client_application_default(): + # [START init_firestore_client_application_default] + import firebase_admin + from firebase_admin import credentials + from firebase_admin import firestore + + # Use the application default credentials. + cred = credentials.ApplicationDefault() + + firebase_admin.initialize_app(cred) + db = firestore.client() + # [END init_firestore_client_application_default] + +def init_firestore_client_service_account(): + # [START init_firestore_client_service_account] + import firebase_admin + from firebase_admin import credentials + from firebase_admin import firestore + + # Use a service account. + cred = credentials.Certificate('path/to/serviceAccount.json') + + app = firebase_admin.initialize_app(cred) + + db = firestore.client() + # [END init_firestore_client_service_account] + +def read_data(): + import firebase_admin + from firebase_admin import firestore + + app = firebase_admin.initialize_app() + db = firestore.client() + + # [START read_data] + doc_ref = db.collection('users').document('alovelace') + doc = doc_ref.get() + if doc.exists: + return f'data: {doc.to_dict()}' + return "Document does not exist." + # [END read_data] + +def add_data(): + import firebase_admin + from firebase_admin import firestore + + app = firebase_admin.initialize_app() + db = firestore.client() + + # [START add_data] + doc_ref = db.collection("users").document("alovelace") + doc_ref.set({ + "first": "Ada", + "last": "Lovelace", + "born": 1815 + }) + # [END add_data] diff --git a/snippets/firestore/firestore_async.py b/snippets/firestore/firestore_async.py new file mode 100644 index 000000000..cf815504e --- /dev/null +++ b/snippets/firestore/firestore_async.py @@ -0,0 +1,132 @@ +# Copyright 2022 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import asyncio + +from firebase_admin import firestore_async + +# pylint: disable=invalid-name +def init_firestore_async_client(): + # [START init_firestore_async_client] + import firebase_admin + from firebase_admin import firestore_async + + # Application Default credentials are automatically created. + app = firebase_admin.initialize_app() + db = firestore_async.client() + # [END init_firestore_async_client] + +def init_firestore_async_client_application_default(): + # [START init_firestore_async_client_application_default] + import firebase_admin + from firebase_admin import credentials + from firebase_admin import firestore_async + + # Use the application default credentials. + cred = credentials.ApplicationDefault() + + firebase_admin.initialize_app(cred) + db = firestore_async.client() + # [END init_firestore_async_client_application_default] + +def init_firestore_async_client_service_account(): + # [START init_firestore_async_client_service_account] + import firebase_admin + from firebase_admin import credentials + from firebase_admin import firestore_async + + # Use a service account. + cred = credentials.Certificate('path/to/serviceAccount.json') + + app = firebase_admin.initialize_app(cred) + + db = firestore_async.client() + # [END init_firestore_async_client_service_account] + +def close_async_sessions(): + import firebase_admin + from firebase_admin import firestore_async + + # [START close_async_sessions] + app = firebase_admin.initialize_app() + db = firestore_async.client() + + # Perform firestore tasks... + + # Delete app to ensure that all the async sessions are closed gracefully. + firebase_admin.delete_app(app) + # [END close_async_sessions] + +async def read_data(): + import firebase_admin + from firebase_admin import firestore_async + + app = firebase_admin.initialize_app() + db = firestore_async.client() + + # [START read_data] + doc_ref = db.collection('users').document('alovelace') + doc = await doc_ref.get() + if doc.exists: + return f'data: {doc.to_dict()}' + # [END read_data] + +async def add_data(): + import firebase_admin + from firebase_admin import firestore_async + + app = firebase_admin.initialize_app() + db = firestore_async.client() + + # [START add_data] + doc_ref = db.collection("users").document("alovelace") + await doc_ref.set({ + "first": "Ada", + "last": "Lovelace", + "born": 1815 + }) + # [END add_data] + +def firestore_async_client_with_asyncio_eventloop(): + # [START firestore_async_client_with_asyncio_eventloop] + import asyncio + import firebase_admin + from firebase_admin import firestore_async + + app = firebase_admin.initialize_app() + db = firestore_async.client() + + # Create coroutine to add user data. + async def add_data(): + doc_ref = db.collection("users").document("alovelace") + print("Start adding user...") + await doc_ref.set({ + "first": "Ada", + "last": "Lovelace", + "born": 1815 + }) + print("Done adding user!") + + # Another corutine with secondary tasks we want to complete. + async def while_waiting(): + print("Start other tasks...") + await asyncio.sleep(2) + print("Finished with other tasks!") + + # Initialize an eventloop to execute tasks until completion. + loop = asyncio.get_event_loop() + tasks = [add_data(), while_waiting()] + loop.run_until_complete(asyncio.gather(*tasks)) + firebase_admin.delete_app(app) + # [END firestore_async_client_with_asyncio_eventloop]