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