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