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