Skip to content

Commit eaba25e

Browse files
authored
feat: create async interface (#61)
* feat: add async tests for AsyncClient * feat: add AsyncClient implementation * feat: add AsyncDocument implementation * feat: add AsyncDocument support to AsyncClient * feat: add AsyncDocument tests Note: tests relying on Collection will fail in this commit * feat: add AsyncCollectionReference class * feat: integrate AsyncCollectionReference * feat: add async_collection tests * fix: swap coroutine/function declaration in async_collection * feat: add async_batch implementation * feat: integrate async_batch * feat: add async_batch tests * feat: add async_query implementation * feat: add async_query integration * feat: add async_query tests * fix: AsyncQuery.get async_generator nesting * feat: add async_transaction integration and tests * fix: linter errors * feat: refactor async tests to use aiounittest and pytest-asyncio * feat: remove duplicate code from async_client * feat: remove duplicate code from async_batch * feat: remove duplicate code from async_collection * feat: remove duplicate code from async_document * fix: remove unused imports * fix: remove duplicate test * feat: remove duplicate code from async_transaction * fix: remove unused Python2 compatibility * fix: resolve async generator tests * fix: create mock async generator to get full coverage * fix: copyright date * feat: create Client/AsyncClient superclass * fix: base client test class * feat: create WriteBatch/AsyncWriteBatch superclass * feat: create CollectionReference/AsyncCollectionReference superclass * feat: create DocumentReference/AsyncDocumentReference superclass * fix: base document test class name * feat: create Query/AsyncQuery superclass * refactor: generalize collection tests with mocks * feat: create Transaction/AsyncTransaction superclass * feat: add microgen support to async interface * fix: async client copyright date * fix: standardize assert syntax * fix: incorrect copyright date * fix: incorrect copyright date * fix: clarify _sleep assertions in transaction * fix: clarify error in context manager tests * fix: clarify error in context manager tests
1 parent cb8606a commit eaba25e

17 files changed

+4531
-14
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2020 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Helpers for batch requests to the Google Cloud Firestore API."""
16+
17+
18+
from google.cloud.firestore_v1.base_batch import BaseWriteBatch
19+
20+
21+
class AsyncWriteBatch(BaseWriteBatch):
22+
"""Accumulate write operations to be sent in a batch.
23+
24+
This has the same set of methods for write operations that
25+
:class:`~google.cloud.firestore_v1.async_document.AsyncDocumentReference` does,
26+
e.g. :meth:`~google.cloud.firestore_v1.async_document.AsyncDocumentReference.create`.
27+
28+
Args:
29+
client (:class:`~google.cloud.firestore_v1.async_client.AsyncClient`):
30+
The client that created this batch.
31+
"""
32+
33+
def __init__(self, client):
34+
super(AsyncWriteBatch, self).__init__(client=client)
35+
36+
async def commit(self):
37+
"""Commit the changes accumulated in this batch.
38+
39+
Returns:
40+
List[:class:`google.cloud.proto.firestore.v1.write.WriteResult`, ...]:
41+
The write results corresponding to the changes committed, returned
42+
in the same order as the changes were applied to this batch. A
43+
write result contains an ``update_time`` field.
44+
"""
45+
commit_response = self._client._firestore_api.commit(
46+
request={
47+
"database": self._client._database_string,
48+
"writes": self._write_pbs,
49+
"transaction": None,
50+
},
51+
metadata=self._client._rpc_metadata,
52+
)
53+
54+
self._write_pbs = []
55+
self.write_results = results = list(commit_response.write_results)
56+
self.commit_time = commit_response.commit_time
57+
return results
58+
59+
async def __aenter__(self):
60+
return self
61+
62+
async def __aexit__(self, exc_type, exc_value, traceback):
63+
if exc_type is None:
64+
await self.commit()
Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
# Copyright 2020 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Client for interacting with the Google Cloud Firestore API.
16+
17+
This is the base from which all interactions with the API occur.
18+
19+
In the hierarchy of API concepts
20+
21+
* a :class:`~google.cloud.firestore_v1.client.Client` owns a
22+
:class:`~google.cloud.firestore_v1.async_collection.AsyncCollectionReference`
23+
* a :class:`~google.cloud.firestore_v1.client.Client` owns a
24+
:class:`~google.cloud.firestore_v1.async_document.AsyncDocumentReference`
25+
"""
26+
27+
from google.cloud.firestore_v1.base_client import (
28+
BaseClient,
29+
DEFAULT_DATABASE,
30+
_CLIENT_INFO,
31+
_reference_info,
32+
_parse_batch_get,
33+
_get_doc_mask,
34+
_path_helper,
35+
)
36+
37+
from google.cloud.firestore_v1 import _helpers
38+
from google.cloud.firestore_v1.async_query import AsyncQuery
39+
from google.cloud.firestore_v1.async_batch import AsyncWriteBatch
40+
from google.cloud.firestore_v1.async_collection import AsyncCollectionReference
41+
from google.cloud.firestore_v1.async_document import AsyncDocumentReference
42+
from google.cloud.firestore_v1.async_transaction import AsyncTransaction
43+
44+
45+
class AsyncClient(BaseClient):
46+
"""Client for interacting with Google Cloud Firestore API.
47+
48+
.. note::
49+
50+
Since the Cloud Firestore API requires the gRPC transport, no
51+
``_http`` argument is accepted by this class.
52+
53+
Args:
54+
project (Optional[str]): The project which the client acts on behalf
55+
of. If not passed, falls back to the default inferred
56+
from the environment.
57+
credentials (Optional[~google.auth.credentials.Credentials]): The
58+
OAuth2 Credentials to use for this client. If not passed, falls
59+
back to the default inferred from the environment.
60+
database (Optional[str]): The database name that the client targets.
61+
For now, :attr:`DEFAULT_DATABASE` (the default value) is the
62+
only valid database.
63+
client_info (Optional[google.api_core.gapic_v1.client_info.ClientInfo]):
64+
The client info used to send a user-agent string along with API
65+
requests. If ``None``, then default info will be used. Generally,
66+
you only need to set this if you're developing your own library
67+
or partner tool.
68+
client_options (Union[dict, google.api_core.client_options.ClientOptions]):
69+
Client options used to set user options on the client. API Endpoint
70+
should be set through client_options.
71+
"""
72+
73+
def __init__(
74+
self,
75+
project=None,
76+
credentials=None,
77+
database=DEFAULT_DATABASE,
78+
client_info=_CLIENT_INFO,
79+
client_options=None,
80+
):
81+
super(AsyncClient, self).__init__(
82+
project=project,
83+
credentials=credentials,
84+
database=database,
85+
client_info=client_info,
86+
client_options=client_options,
87+
)
88+
89+
def collection(self, *collection_path):
90+
"""Get a reference to a collection.
91+
92+
For a top-level collection:
93+
94+
.. code-block:: python
95+
96+
>>> client.collection('top')
97+
98+
For a sub-collection:
99+
100+
.. code-block:: python
101+
102+
>>> client.collection('mydocs/doc/subcol')
103+
>>> # is the same as
104+
>>> client.collection('mydocs', 'doc', 'subcol')
105+
106+
Sub-collections can be nested deeper in a similar fashion.
107+
108+
Args:
109+
collection_path (Tuple[str, ...]): Can either be
110+
111+
* A single ``/``-delimited path to a collection
112+
* A tuple of collection path segments
113+
114+
Returns:
115+
:class:`~google.cloud.firestore_v1.async_collection.AsyncCollectionReference`:
116+
A reference to a collection in the Firestore database.
117+
"""
118+
return AsyncCollectionReference(*_path_helper(collection_path), client=self)
119+
120+
def collection_group(self, collection_id):
121+
"""
122+
Creates and returns a new AsyncQuery that includes all documents in the
123+
database that are contained in a collection or subcollection with the
124+
given collection_id.
125+
126+
.. code-block:: python
127+
128+
>>> query = client.collection_group('mygroup')
129+
130+
Args:
131+
collection_id (str) Identifies the collections to query over.
132+
133+
Every collection or subcollection with this ID as the last segment of its
134+
path will be included. Cannot contain a slash.
135+
136+
Returns:
137+
:class:`~google.cloud.firestore_v1.async_query.AsyncQuery`:
138+
The created AsyncQuery.
139+
"""
140+
return AsyncQuery(
141+
self._get_collection_reference(collection_id), all_descendants=True
142+
)
143+
144+
def document(self, *document_path):
145+
"""Get a reference to a document in a collection.
146+
147+
For a top-level document:
148+
149+
.. code-block:: python
150+
151+
>>> client.document('collek/shun')
152+
>>> # is the same as
153+
>>> client.document('collek', 'shun')
154+
155+
For a document in a sub-collection:
156+
157+
.. code-block:: python
158+
159+
>>> client.document('mydocs/doc/subcol/child')
160+
>>> # is the same as
161+
>>> client.document('mydocs', 'doc', 'subcol', 'child')
162+
163+
Documents in sub-collections can be nested deeper in a similar fashion.
164+
165+
Args:
166+
document_path (Tuple[str, ...]): Can either be
167+
168+
* A single ``/``-delimited path to a document
169+
* A tuple of document path segments
170+
171+
Returns:
172+
:class:`~google.cloud.firestore_v1.document.AsyncDocumentReference`:
173+
A reference to a document in a collection.
174+
"""
175+
return AsyncDocumentReference(
176+
*self._document_path_helper(*document_path), client=self
177+
)
178+
179+
async def get_all(self, references, field_paths=None, transaction=None):
180+
"""Retrieve a batch of documents.
181+
182+
.. note::
183+
184+
Documents returned by this method are not guaranteed to be
185+
returned in the same order that they are given in ``references``.
186+
187+
.. note::
188+
189+
If multiple ``references`` refer to the same document, the server
190+
will only return one result.
191+
192+
See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for
193+
more information on **field paths**.
194+
195+
If a ``transaction`` is used and it already has write operations
196+
added, this method cannot be used (i.e. read-after-write is not
197+
allowed).
198+
199+
Args:
200+
references (List[.AsyncDocumentReference, ...]): Iterable of document
201+
references to be retrieved.
202+
field_paths (Optional[Iterable[str, ...]]): An iterable of field
203+
paths (``.``-delimited list of field names) to use as a
204+
projection of document fields in the returned results. If
205+
no value is provided, all fields will be returned.
206+
transaction (Optional[:class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction`]):
207+
An existing transaction that these ``references`` will be
208+
retrieved in.
209+
210+
Yields:
211+
.DocumentSnapshot: The next document snapshot that fulfills the
212+
query, or :data:`None` if the document does not exist.
213+
"""
214+
document_paths, reference_map = _reference_info(references)
215+
mask = _get_doc_mask(field_paths)
216+
response_iterator = self._firestore_api.batch_get_documents(
217+
request={
218+
"database": self._database_string,
219+
"documents": document_paths,
220+
"mask": mask,
221+
"transaction": _helpers.get_transaction_id(transaction),
222+
},
223+
metadata=self._rpc_metadata,
224+
)
225+
226+
for get_doc_response in response_iterator:
227+
yield _parse_batch_get(get_doc_response, reference_map, self)
228+
229+
async def collections(self):
230+
"""List top-level collections of the client's database.
231+
232+
Returns:
233+
Sequence[:class:`~google.cloud.firestore_v1.async_collection.AsyncCollectionReference`]:
234+
iterator of subcollections of the current document.
235+
"""
236+
iterator = self._firestore_api.list_collection_ids(
237+
request={"parent": "{}/documents".format(self._database_string)},
238+
metadata=self._rpc_metadata,
239+
)
240+
241+
while True:
242+
for i in iterator.collection_ids:
243+
yield self.collection(i)
244+
if iterator.next_page_token:
245+
iterator = self._firestore_api.list_collection_ids(
246+
request={
247+
"parent": "{}/documents".format(self._database_string),
248+
"page_token": iterator.next_page_token,
249+
},
250+
metadata=self._rpc_metadata,
251+
)
252+
else:
253+
return
254+
255+
# TODO(microgen): currently this method is rewritten to iterate/page itself.
256+
# https://github.com/googleapis/gapic-generator-python/issues/516
257+
# it seems the generator ought to be able to do this itself.
258+
# iterator.client = self
259+
# iterator.item_to_value = _item_to_collection_ref
260+
# return iterator
261+
262+
def batch(self):
263+
"""Get a batch instance from this client.
264+
265+
Returns:
266+
:class:`~google.cloud.firestore_v1.async_batch.AsyncWriteBatch`:
267+
A "write" batch to be used for accumulating document changes and
268+
sending the changes all at once.
269+
"""
270+
return AsyncWriteBatch(self)
271+
272+
def transaction(self, **kwargs):
273+
"""Get a transaction that uses this client.
274+
275+
See :class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction` for
276+
more information on transactions and the constructor arguments.
277+
278+
Args:
279+
kwargs (Dict[str, Any]): The keyword arguments (other than
280+
``client``) to pass along to the
281+
:class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction`
282+
constructor.
283+
284+
Returns:
285+
:class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction`:
286+
A transaction attached to this client.
287+
"""
288+
return AsyncTransaction(self, **kwargs)

0 commit comments

Comments
 (0)