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