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