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
17This is the base from which all interactions with the API occur.
18
19In 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"""
26from __future__ import annotations
27
28from typing import (
29 TYPE_CHECKING,
30 Any,
31 AsyncGenerator,
32 Iterable,
33 List,
34 Optional,
35 Union,
36)
37
38from google.api_core import gapic_v1
39from google.api_core import retry_async as retries
40
41from google.cloud.firestore_v1.async_batch import AsyncWriteBatch
42from google.cloud.firestore_v1.async_collection import AsyncCollectionReference
43from google.cloud.firestore_v1.async_document import (
44 AsyncDocumentReference,
45 DocumentSnapshot,
46)
47from google.cloud.firestore_v1.async_query import AsyncCollectionGroup
48from google.cloud.firestore_v1.async_transaction import AsyncTransaction
49from google.cloud.firestore_v1.base_client import _parse_batch_get # type: ignore
50from google.cloud.firestore_v1.base_client import _CLIENT_INFO, BaseClient, _path_helper
51from google.cloud.firestore_v1.base_transaction import MAX_ATTEMPTS
52from google.cloud.firestore_v1.field_path import FieldPath
53from google.cloud.firestore_v1.services.firestore import (
54 async_client as firestore_client,
55)
56from google.cloud.firestore_v1.services.firestore.transports import (
57 grpc_asyncio as firestore_grpc_transport,
58)
59from google.cloud.firestore_v1.async_pipeline import AsyncPipeline
60from google.cloud.firestore_v1.pipeline_source import PipelineSource
61
62if TYPE_CHECKING: # pragma: NO COVER
63 import datetime
64
65 from google.cloud.firestore_v1.bulk_writer import BulkWriter
66
67
68class AsyncClient(BaseClient):
69 """Client for interacting with Google Cloud Firestore API.
70
71 .. note::
72
73 Since the Cloud Firestore API requires the gRPC transport, no
74 ``_http`` argument is accepted by this class.
75
76 Args:
77 project (Optional[str]): The project which the client acts on behalf
78 of. If not passed, falls back to the default inferred
79 from the environment.
80 credentials (Optional[~google.auth.credentials.Credentials]): The
81 OAuth2 Credentials to use for this client. If not passed, falls
82 back to the default inferred from the environment.
83 database (Optional[str]): The database name that the client targets.
84 For now, :attr:`DEFAULT_DATABASE` (the default value) is the
85 only valid database.
86 client_info (Optional[google.api_core.gapic_v1.client_info.ClientInfo]):
87 The client info used to send a user-agent string along with API
88 requests. If ``None``, then default info will be used. Generally,
89 you only need to set this if you're developing your own library
90 or partner tool.
91 client_options (Union[dict, google.api_core.client_options.ClientOptions]):
92 Client options used to set user options on the client. API Endpoint
93 should be set through client_options.
94 """
95
96 def __init__(
97 self,
98 project=None,
99 credentials=None,
100 database=None,
101 client_info=_CLIENT_INFO,
102 client_options=None,
103 ) -> None:
104 super(AsyncClient, self).__init__(
105 project=project,
106 credentials=credentials,
107 database=database,
108 client_info=client_info,
109 client_options=client_options,
110 )
111
112 def _to_sync_copy(self):
113 from google.cloud.firestore_v1.client import Client
114
115 if not getattr(self, "_sync_copy", None):
116 self._sync_copy = Client(
117 project=self.project,
118 credentials=self._credentials,
119 database=self._database,
120 client_info=self._client_info,
121 client_options=self._client_options,
122 )
123 return self._sync_copy
124
125 @property
126 def _firestore_api(self):
127 """Lazy-loading getter GAPIC Firestore API.
128 Returns:
129 :class:`~google.cloud.gapic.firestore.v1`.async_firestore_client.FirestoreAsyncClient:
130 The GAPIC client with the credentials of the current client.
131 """
132 return self._firestore_api_helper(
133 firestore_grpc_transport.FirestoreGrpcAsyncIOTransport,
134 firestore_client.FirestoreAsyncClient,
135 firestore_client,
136 )
137
138 @property
139 def _target(self):
140 """Return the target (where the API is).
141 Eg. "firestore.googleapis.com"
142
143 Returns:
144 str: The location of the API.
145 """
146 return self._target_helper(firestore_client.FirestoreAsyncClient)
147
148 def collection(self, *collection_path: str) -> AsyncCollectionReference:
149 """Get a reference to a collection.
150
151 For a top-level collection:
152
153 .. code-block:: python
154
155 >>> client.collection('top')
156
157 For a sub-collection:
158
159 .. code-block:: python
160
161 >>> client.collection('mydocs/doc/subcol')
162 >>> # is the same as
163 >>> client.collection('mydocs', 'doc', 'subcol')
164
165 Sub-collections can be nested deeper in a similar fashion.
166
167 Args:
168 collection_path: Can either be
169
170 * A single ``/``-delimited path to a collection
171 * A tuple of collection path segments
172
173 Returns:
174 :class:`~google.cloud.firestore_v1.async_collection.AsyncCollectionReference`:
175 A reference to a collection in the Firestore database.
176 """
177 return AsyncCollectionReference(*_path_helper(collection_path), client=self)
178
179 def collection_group(self, collection_id: str) -> AsyncCollectionGroup:
180 """
181 Creates and returns a new AsyncQuery that includes all documents in the
182 database that are contained in a collection or subcollection with the
183 given collection_id.
184
185 .. code-block:: python
186
187 >>> query = client.collection_group('mygroup')
188
189 Args:
190 collection_id (str) Identifies the collections to query over.
191
192 Every collection or subcollection with this ID as the last segment of its
193 path will be included. Cannot contain a slash.
194
195 Returns:
196 :class:`~google.cloud.firestore_v1.async_query.AsyncCollectionGroup`:
197 The created AsyncQuery.
198 """
199 return AsyncCollectionGroup(self._get_collection_reference(collection_id))
200
201 def document(self, *document_path: str) -> AsyncDocumentReference:
202 """Get a reference to a document in a collection.
203
204 For a top-level document:
205
206 .. code-block:: python
207
208 >>> client.document('collek/shun')
209 >>> # is the same as
210 >>> client.document('collek', 'shun')
211
212 For a document in a sub-collection:
213
214 .. code-block:: python
215
216 >>> client.document('mydocs/doc/subcol/child')
217 >>> # is the same as
218 >>> client.document('mydocs', 'doc', 'subcol', 'child')
219
220 Documents in sub-collections can be nested deeper in a similar fashion.
221
222 Args:
223 document_path: Can either be
224
225 * A single ``/``-delimited path to a document
226 * A tuple of document path segments
227
228 Returns:
229 :class:`~google.cloud.firestore_v1.document.AsyncDocumentReference`:
230 A reference to a document in a collection.
231 """
232 return AsyncDocumentReference(
233 *self._document_path_helper(*document_path), client=self
234 )
235
236 async def get_all(
237 self,
238 references: List[AsyncDocumentReference],
239 field_paths: Iterable[str] | None = None,
240 transaction: AsyncTransaction | None = None,
241 retry: retries.AsyncRetry | object | None = gapic_v1.method.DEFAULT,
242 timeout: float | None = None,
243 *,
244 read_time: datetime.datetime | None = None,
245 ) -> AsyncGenerator[DocumentSnapshot, Any]:
246 """Retrieve a batch of documents.
247
248 .. note::
249
250 Documents returned by this method are not guaranteed to be
251 returned in the same order that they are given in ``references``.
252
253 .. note::
254
255 If multiple ``references`` refer to the same document, the server
256 will only return one result.
257
258 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for
259 more information on **field paths**.
260
261 If a ``transaction`` is used and it already has write operations
262 added, this method cannot be used (i.e. read-after-write is not
263 allowed).
264
265 Args:
266 references (List[.AsyncDocumentReference, ...]): Iterable of document
267 references to be retrieved.
268 field_paths (Optional[Iterable[str, ...]]): An iterable of field
269 paths (``.``-delimited list of field names) to use as a
270 projection of document fields in the returned results. If
271 no value is provided, all fields will be returned.
272 transaction (Optional[:class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction`]):
273 An existing transaction that these ``references`` will be
274 retrieved in.
275 retry (google.api_core.retry.Retry): Designation of what errors, if any,
276 should be retried. Defaults to a system-specified policy.
277 timeout (float): The timeout for this request. Defaults to a
278 system-specified value.
279 read_time (Optional[datetime.datetime]): If set, reads documents as they were at the given
280 time. This must be a timestamp within the past one hour, or if Point-in-Time Recovery
281 is enabled, can additionally be a whole minute timestamp within the past 7 days. If no
282 timezone is specified in the :class:`datetime.datetime` object, it is assumed to be UTC.
283
284 Yields:
285 .DocumentSnapshot: The next document snapshot that fulfills the
286 query, or :data:`None` if the document does not exist.
287 """
288 request, reference_map, kwargs = self._prep_get_all(
289 references, field_paths, transaction, retry, timeout, read_time
290 )
291
292 response_iterator = await self._firestore_api.batch_get_documents(
293 request=request,
294 metadata=self._rpc_metadata,
295 **kwargs,
296 )
297
298 async for get_doc_response in response_iterator:
299 yield _parse_batch_get(get_doc_response, reference_map, self)
300
301 async def collections(
302 self,
303 retry: retries.AsyncRetry | object | None = gapic_v1.method.DEFAULT,
304 timeout: float | None = None,
305 *,
306 read_time: datetime.datetime | None = None,
307 ) -> AsyncGenerator[AsyncCollectionReference, Any]:
308 """List top-level collections of the client's database.
309
310 Args:
311 retry (google.api_core.retry.Retry): Designation of what errors, if any,
312 should be retried. Defaults to a system-specified policy.
313 timeout (float): The timeout for this request. Defaults to a
314 system-specified value.
315 read_time (Optional[datetime.datetime]): If set, reads documents as they were at the given
316 time. This must be a timestamp within the past one hour, or if Point-in-Time Recovery
317 is enabled, can additionally be a whole minute timestamp within the past 7 days. If no
318 timezone is specified in the :class:`datetime.datetime` object, it is assumed to be UTC.
319
320 Returns:
321 Sequence[:class:`~google.cloud.firestore_v1.async_collection.AsyncCollectionReference`]:
322 iterator of subcollections of the current document.
323 """
324 request, kwargs = self._prep_collections(retry, timeout, read_time)
325 iterator = await self._firestore_api.list_collection_ids(
326 request=request,
327 metadata=self._rpc_metadata,
328 **kwargs,
329 )
330
331 async for collection_id in iterator:
332 yield self.collection(collection_id)
333
334 async def recursive_delete(
335 self,
336 reference: Union[AsyncCollectionReference, AsyncDocumentReference],
337 *,
338 bulk_writer: Optional["BulkWriter"] = None,
339 chunk_size: int = 5000,
340 ) -> int:
341 """Deletes documents and their subcollections, regardless of collection
342 name.
343
344 Passing an AsyncCollectionReference leads to each document in the
345 collection getting deleted, as well as all of their descendents.
346
347 Passing an AsyncDocumentReference deletes that one document and all of
348 its descendents.
349
350 Args:
351 reference (Union[
352 :class:`@google.cloud.firestore_v1.async_collection.CollectionReference`,
353 :class:`@google.cloud.firestore_v1.async_document.DocumentReference`,
354 ])
355 The reference to be deleted.
356
357 bulk_writer (Optional[:class:`@google.cloud.firestore_v1.bulk_writer.BulkWriter`])
358 The BulkWriter used to delete all matching documents. Supply this
359 if you want to override the default throttling behavior.
360 """
361 if bulk_writer is None:
362 bulk_writer = self.bulk_writer()
363
364 return await self._recursive_delete(
365 reference,
366 bulk_writer=bulk_writer,
367 chunk_size=chunk_size,
368 )
369
370 async def _recursive_delete(
371 self,
372 reference: Union[AsyncCollectionReference, AsyncDocumentReference],
373 bulk_writer: "BulkWriter",
374 *,
375 chunk_size: int = 5000,
376 depth: int = 0,
377 ) -> int:
378 """Recursion helper for `recursive_delete."""
379
380 num_deleted: int = 0
381
382 if isinstance(reference, AsyncCollectionReference):
383 chunk: List[DocumentSnapshot]
384 async for chunk in reference.recursive().select(
385 [FieldPath.document_id()]
386 )._chunkify(chunk_size):
387 doc_snap: DocumentSnapshot
388 for doc_snap in chunk:
389 num_deleted += 1
390 bulk_writer.delete(doc_snap.reference)
391
392 elif isinstance(reference, AsyncDocumentReference):
393 col_ref: AsyncCollectionReference
394 async for col_ref in reference.collections():
395 num_deleted += await self._recursive_delete(
396 col_ref,
397 bulk_writer=bulk_writer,
398 depth=depth + 1,
399 chunk_size=chunk_size,
400 )
401 num_deleted += 1
402 bulk_writer.delete(reference)
403
404 else:
405 raise TypeError(
406 f"Unexpected type for reference: {reference.__class__.__name__}"
407 )
408
409 if depth == 0:
410 bulk_writer.close()
411
412 return num_deleted
413
414 def batch(self) -> AsyncWriteBatch:
415 """Get a batch instance from this client.
416
417 Returns:
418 :class:`~google.cloud.firestore_v1.async_batch.AsyncWriteBatch`:
419 A "write" batch to be used for accumulating document changes and
420 sending the changes all at once.
421 """
422 return AsyncWriteBatch(self)
423
424 def transaction(
425 self, max_attempts: int = MAX_ATTEMPTS, read_only: bool = False
426 ) -> AsyncTransaction:
427 """Get a transaction that uses this client.
428
429 See :class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction` for
430 more information on transactions and the constructor arguments.
431
432 Args:
433 kwargs (Dict[str, Any]): The keyword arguments (other than
434 ``client``) to pass along to the
435 :class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction`
436 constructor.
437
438 Returns:
439 :class:`~google.cloud.firestore_v1.async_transaction.AsyncTransaction`:
440 A transaction attached to this client.
441 """
442 return AsyncTransaction(self, max_attempts=max_attempts, read_only=read_only)
443
444 @property
445 def _pipeline_cls(self):
446 return AsyncPipeline
447
448 def pipeline(self) -> PipelineSource:
449 return PipelineSource(self)