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)