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"""Classes for representing documents for the Google Cloud Firestore API."""
16from __future__ import annotations
17import datetime
18import logging
19from typing import Any, Callable, Generator, Iterable
20
21from google.api_core import gapic_v1
22from google.api_core import retry as retries
23from google.cloud._helpers import _datetime_to_pb_timestamp # type: ignore
24from google.protobuf.timestamp_pb2 import Timestamp
25
26from google.cloud.firestore_v1 import _helpers
27from google.cloud.firestore_v1.base_document import (
28 BaseDocumentReference,
29 DocumentSnapshot,
30 _first_write_result,
31)
32from google.cloud.firestore_v1.types import write
33from google.cloud.firestore_v1.watch import Watch
34
35logger = logging.getLogger(__name__)
36
37
38class DocumentReference(BaseDocumentReference):
39 """A reference to a document in a Firestore database.
40
41 The document may already exist or can be created by this class.
42
43 Args:
44 path (Tuple[str, ...]): The components in the document path.
45 This is a series of strings representing each collection and
46 sub-collection ID, as well as the document IDs for any documents
47 that contain a sub-collection (as well as the base document).
48 kwargs (dict): The keyword arguments for the constructor. The only
49 supported keyword is ``client`` and it must be a
50 :class:`~google.cloud.firestore_v1.client.Client`. It represents
51 the client that created this document reference.
52
53 Raises:
54 ValueError: if
55
56 * the ``path`` is empty
57 * there are an even number of elements
58 * a collection ID in ``path`` is not a string
59 * a document ID in ``path`` is not a string
60 TypeError: If a keyword other than ``client`` is used.
61 """
62
63 def __init__(self, *path, **kwargs) -> None:
64 super(DocumentReference, self).__init__(*path, **kwargs)
65
66 def create(
67 self,
68 document_data: dict,
69 retry: retries.Retry | object | None = gapic_v1.method.DEFAULT,
70 timeout: float | None = None,
71 ) -> write.WriteResult:
72 """Create a document in the Firestore database.
73
74 >>> document_data = {"a": 1, "b": {"c": "Two"}}
75 >>> document.get().to_dict() is None # does not exist
76 True
77 >>> document.create(document_data)
78 >>> document.get().to_dict() == document_data # exists
79 True
80
81 Args:
82 document_data (dict): Property names and values to use for
83 creating a document.
84 retry (google.api_core.retry.Retry): Designation of what errors, if any,
85 should be retried. Defaults to a system-specified policy.
86 timeout (float): The timeout for this request. Defaults to a
87 system-specified value.
88
89 Returns:
90 :class:`~google.cloud.firestore_v1.types.WriteResult`:
91 The write result corresponding to the committed document.
92 A write result contains an ``update_time`` field.
93
94 Raises:
95 :class:`google.cloud.exceptions.Conflict`:
96 If the document already exists.
97 """
98 batch, kwargs = self._prep_create(document_data, retry, timeout)
99 write_results = batch.commit(**kwargs)
100 return _first_write_result(write_results)
101
102 def set(
103 self,
104 document_data: dict,
105 merge: bool = False,
106 retry: retries.Retry | object | None = gapic_v1.method.DEFAULT,
107 timeout: float | None = None,
108 ) -> write.WriteResult:
109 """Create / replace / merge a document in the Firestore database.
110
111 - To "upsert" a document (create if it doesn't exist, replace completely
112 if it does), leave the ``merge`` argument at its default:
113
114 >>> document_data = {"a": 1, "b": {"c": "Two"}}
115 >>> document.get().to_dict() is None # document exists
116 False
117 >>> document.set(document_data)
118 >>> document.get().to_dict() == document_data # exists
119 True
120
121 - To "merge" ``document_data`` with an existing document (creating if
122 the document does not exist), pass ``merge`` as True``:
123
124 >>> document_data = {"a": 1, "b": {"c": "Two"}}
125 >>> document.get().to_dict() == {"d": "Three", "b": {}} # exists
126 >>> document.set(document_data, merge=True)
127 >>> document.get().to_dict() == {"a": 1, "d": "Three", "b": {"c": "Two"}}
128 True
129
130 In this case, existing documents with top-level keys which are
131 not present in ``document_data`` (``"d"``) will preserve the values
132 of those keys.
133
134
135 - To merge only specific fields of ``document_data`` with existing
136 documents (creating if the document does not exist), pass ``merge``
137 as a list of field paths:
138
139
140 >>> document_data = {"a": 1, "b": {"c": "Two"}}
141 >>> document.get().to_dict() == {"b": {"c": "One", "d": "Four" }} # exists
142 True
143 >>> document.set(document_data, merge=["b.c"])
144 >>> document.get().to_dict() == {"b": {"c": "Two", "d": "Four" }}
145 True
146
147 For more information on field paths, see
148 :meth:`~google.cloud.firestore_v1.base_client.BaseClient.field_path`.
149
150 Args:
151 document_data (dict): Property names and values to use for
152 replacing a document.
153 merge (Optional[bool] or Optional[List<fieldpath>]):
154 If True, apply merging instead of overwriting the state
155 of the document.
156 retry (google.api_core.retry.Retry): Designation of what errors, if any,
157 should be retried. Defaults to a system-specified policy.
158 timeout (float): The timeout for this request. Defaults to a
159 system-specified value.
160
161 Returns:
162 :class:`~google.cloud.firestore_v1.types.WriteResult`:
163 The write result corresponding to the committed document. A write
164 result contains an ``update_time`` field.
165 """
166 batch, kwargs = self._prep_set(document_data, merge, retry, timeout)
167 write_results = batch.commit(**kwargs)
168 return _first_write_result(write_results)
169
170 def update(
171 self,
172 field_updates: dict,
173 option: _helpers.WriteOption | None = None,
174 retry: retries.Retry | object | None = gapic_v1.method.DEFAULT,
175 timeout: float | None = None,
176 ) -> write.WriteResult:
177 """Update an existing document in the Firestore database.
178
179 By default, this method verifies that the document exists on the
180 server before making updates. A write ``option`` can be specified to
181 override these preconditions.
182
183 Each key in ``field_updates`` can either be a field name or a
184 **field path** (For more information on field paths, see
185 :meth:`~google.cloud.firestore_v1.base_client.BaseClient.field_path`.)
186 To illustrate this, consider a document with
187
188 .. code-block:: python
189
190 >>> snapshot = document.get()
191 >>> snapshot.to_dict()
192 {
193 'foo': {
194 'bar': 'baz',
195 },
196 'other': True,
197 }
198
199 stored on the server. If the field name is used in the update:
200
201 .. code-block:: python
202
203 >>> field_updates = {
204 ... 'foo': {
205 ... 'quux': 800,
206 ... },
207 ... }
208 >>> document.update(field_updates)
209
210 then all of ``foo`` will be overwritten on the server and the new
211 value will be
212
213 .. code-block:: python
214
215 >>> snapshot = document.get()
216 >>> snapshot.to_dict()
217 {
218 'foo': {
219 'quux': 800,
220 },
221 'other': True,
222 }
223
224 On the other hand, if a ``.``-delimited **field path** is used in the
225 update:
226
227 .. code-block:: python
228
229 >>> field_updates = {
230 ... 'foo.quux': 800,
231 ... }
232 >>> document.update(field_updates)
233
234 then only ``foo.quux`` will be updated on the server and the
235 field ``foo.bar`` will remain intact:
236
237 .. code-block:: python
238
239 >>> snapshot = document.get()
240 >>> snapshot.to_dict()
241 {
242 'foo': {
243 'bar': 'baz',
244 'quux': 800,
245 },
246 'other': True,
247 }
248
249 .. warning::
250
251 A **field path** can only be used as a top-level key in
252 ``field_updates``.
253
254 To delete / remove a field from an existing document, use the
255 :attr:`~google.cloud.firestore_v1.transforms.DELETE_FIELD` sentinel.
256 So with the example above, sending
257
258 .. code-block:: python
259
260 >>> field_updates = {
261 ... 'other': firestore.DELETE_FIELD,
262 ... }
263 >>> document.update(field_updates)
264
265 would update the value on the server to:
266
267 .. code-block:: python
268
269 >>> snapshot = document.get()
270 >>> snapshot.to_dict()
271 {
272 'foo': {
273 'bar': 'baz',
274 },
275 }
276
277 To set a field to the current time on the server when the
278 update is received, use the
279 :attr:`~google.cloud.firestore_v1.transforms.SERVER_TIMESTAMP`
280 sentinel.
281 Sending
282
283 .. code-block:: python
284
285 >>> field_updates = {
286 ... 'foo.now': firestore.SERVER_TIMESTAMP,
287 ... }
288 >>> document.update(field_updates)
289
290 would update the value on the server to:
291
292 .. code-block:: python
293
294 >>> snapshot = document.get()
295 >>> snapshot.to_dict()
296 {
297 'foo': {
298 'bar': 'baz',
299 'now': datetime.datetime(2012, ...),
300 },
301 'other': True,
302 }
303
304 Args:
305 field_updates (dict): Field names or paths to update and values
306 to update with.
307 option (Optional[:class:`~google.cloud.firestore_v1.client.WriteOption`]):
308 A write option to make assertions / preconditions on the server
309 state of the document before applying changes.
310 retry (google.api_core.retry.Retry): Designation of what errors, if any,
311 should be retried. Defaults to a system-specified policy.
312 timeout (float): The timeout for this request. Defaults to a
313 system-specified value.
314
315 Returns:
316 :class:`~google.cloud.firestore_v1.types.WriteResult`:
317 The write result corresponding to the updated document. A write
318 result contains an ``update_time`` field.
319
320 Raises:
321 :class:`google.cloud.exceptions.NotFound`:
322 If the document does not exist.
323 """
324 batch, kwargs = self._prep_update(field_updates, option, retry, timeout)
325 write_results = batch.commit(**kwargs)
326 return _first_write_result(write_results)
327
328 def delete(
329 self,
330 option: _helpers.WriteOption | None = None,
331 retry: retries.Retry | object | None = gapic_v1.method.DEFAULT,
332 timeout: float | None = None,
333 ) -> Timestamp:
334 """Delete the current document in the Firestore database.
335
336 Args:
337 option (Optional[:class:`~google.cloud.firestore_v1.client.WriteOption`]):
338 A write option to make assertions / preconditions on the server
339 state of the document before applying changes.
340 retry (google.api_core.retry.Retry): Designation of what errors, if any,
341 should be retried. Defaults to a system-specified policy.
342 timeout (float): The timeout for this request. Defaults to a
343 system-specified value.
344
345 Returns:
346 :class:`google.protobuf.timestamp_pb2.Timestamp`:
347 The time that the delete request was received by the server.
348 If the document did not exist when the delete was sent (i.e.
349 nothing was deleted), this method will still succeed and will
350 still return the time that the request was received by the server.
351 """
352 request, kwargs = self._prep_delete(option, retry, timeout)
353
354 commit_response = self._client._firestore_api.commit(
355 request=request,
356 metadata=self._client._rpc_metadata,
357 **kwargs,
358 )
359
360 return commit_response.commit_time
361
362 def get(
363 self,
364 field_paths: Iterable[str] | None = None,
365 transaction=None,
366 retry: retries.Retry | object | None = gapic_v1.method.DEFAULT,
367 timeout: float | None = None,
368 *,
369 read_time: datetime.datetime | None = None,
370 ) -> DocumentSnapshot:
371 """Retrieve a snapshot of the current document.
372
373 See :meth:`~google.cloud.firestore_v1.base_client.BaseClient.field_path` for
374 more information on **field paths**.
375
376 If a ``transaction`` is used and it already has write operations
377 added, this method cannot be used (i.e. read-after-write is not
378 allowed).
379
380 Args:
381 field_paths (Optional[Iterable[str, ...]]): An iterable of field
382 paths (``.``-delimited list of field names) to use as a
383 projection of document fields in the returned results. If
384 no value is provided, all fields will be returned.
385 transaction (Optional[:class:`~google.cloud.firestore_v1.transaction.Transaction`]):
386 An existing transaction that this reference
387 will be retrieved in.
388 retry (google.api_core.retry.Retry): Designation of what errors, if an y,
389 should be retried. Defaults to a system-specified policy.
390 timeout (float): The timeout for this request. Defaults to a
391 system-specified value.
392 read_time (Optional[datetime.datetime]): If set, reads documents as they were at the given
393 time. This must be a timestamp within the past one hour, or if Point-in-Time Recovery
394 is enabled, can additionally be a whole minute timestamp within the past 7 days. If no
395 timezone is specified in the :class:`datetime.datetime` object, it is assumed to be UTC.
396
397 Returns:
398 :class:`~google.cloud.firestore_v1.base_document.DocumentSnapshot`:
399 A snapshot of the current document. If the document does not
400 exist at the time of the snapshot is taken, the snapshot's
401 :attr:`reference`, :attr:`data`, :attr:`update_time`, and
402 :attr:`create_time` attributes will all be ``None`` and
403 its :attr:`exists` attribute will be ``False``.
404 """
405 from google.cloud.firestore_v1.base_client import _parse_batch_get
406
407 request, kwargs = self._prep_batch_get(
408 field_paths, transaction, retry, timeout, read_time
409 )
410
411 response_iter = self._client._firestore_api.batch_get_documents(
412 request=request,
413 metadata=self._client._rpc_metadata,
414 **kwargs,
415 )
416
417 get_doc_response = next(response_iter, None)
418
419 if get_doc_response is not None:
420 return _parse_batch_get(
421 get_doc_response=get_doc_response,
422 reference_map={self._document_path: self},
423 client=self._client,
424 )
425
426 logger.warning(
427 "`batch_get_documents` unexpectedly returned empty "
428 "stream. Expected one object.",
429 )
430
431 return DocumentSnapshot(
432 self,
433 None,
434 exists=False,
435 read_time=_datetime_to_pb_timestamp(datetime.datetime.now()),
436 create_time=None,
437 update_time=None,
438 )
439
440 def collections(
441 self,
442 page_size: int | None = None,
443 retry: retries.Retry | object | None = gapic_v1.method.DEFAULT,
444 timeout: float | None = None,
445 *,
446 read_time: datetime.datetime | None = None,
447 ) -> Generator[Any, Any, None]:
448 """List subcollections of the current document.
449
450 Args:
451 page_size (Optional[int]]): The maximum number of collections
452 in each page of results from this request. Non-positive values
453 are ignored. Defaults to a sensible value set by the API.
454 retry (google.api_core.retry.Retry): Designation of what errors, if any,
455 should be retried. Defaults to a system-specified policy.
456 timeout (float): The timeout for this request. Defaults to a
457 system-specified value.
458 read_time (Optional[datetime.datetime]): If set, reads documents as they were at the given
459 time. This must be a timestamp within the past one hour, or if Point-in-Time Recovery
460 is enabled, can additionally be a whole minute timestamp within the past 7 days. If no
461 timezone is specified in the :class:`datetime.datetime` object, it is assumed to be UTC.
462
463 Returns:
464 Sequence[:class:`~google.cloud.firestore_v1.collection.CollectionReference`]:
465 iterator of subcollections of the current document. If the
466 document does not exist at the time of `snapshot`, the
467 iterator will be empty
468 """
469 request, kwargs = self._prep_collections(page_size, retry, timeout, read_time)
470
471 iterator = self._client._firestore_api.list_collection_ids(
472 request=request,
473 metadata=self._client._rpc_metadata,
474 **kwargs,
475 )
476
477 for collection_id in iterator:
478 yield self.collection(collection_id)
479
480 def on_snapshot(self, callback: Callable) -> Watch:
481 """Watch this document.
482
483 This starts a watch on this document using a background thread. The
484 provided callback is run on the snapshot.
485
486 Args:
487 callback(Callable[[:class:`~google.cloud.firestore_v1.base_document.DocumentSnapshot`], NoneType]):
488 a callback to run when a change occurs
489
490 Example:
491
492 .. code-block:: python
493
494 from google.cloud import firestore_v1
495
496 db = firestore_v1.Client()
497 collection_ref = db.collection(u'users')
498
499 def on_snapshot(document_snapshot, changes, read_time):
500 doc = document_snapshot
501 print(u'{} => {}'.format(doc.id, doc.to_dict()))
502
503 doc_ref = db.collection(u'users').document(
504 u'alovelace' + unique_resource_id())
505
506 # Watch this document
507 doc_watch = doc_ref.on_snapshot(on_snapshot)
508
509 # Terminate this watch
510 doc_watch.unsubscribe()
511 """
512 return Watch.for_document(self, callback, DocumentSnapshot)