Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/google/cloud/firestore_v1/base_collection.py: 39%
125 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 06:27 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-09 06:27 +0000
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.
15"""Classes for representing collections for the Google Cloud Firestore API."""
16from __future__ import annotations
17import random
19from google.api_core import retry as retries
21from google.cloud.firestore_v1 import _helpers
22from google.cloud.firestore_v1.document import DocumentReference
23from google.cloud.firestore_v1.base_aggregation import BaseAggregationQuery
24from google.cloud.firestore_v1.base_query import QueryType
27from typing import (
28 Optional,
29 Any,
30 AsyncGenerator,
31 Coroutine,
32 Generator,
33 Generic,
34 AsyncIterator,
35 Iterator,
36 Iterable,
37 NoReturn,
38 Tuple,
39 Union,
40 TYPE_CHECKING,
41)
44if TYPE_CHECKING: # pragma: NO COVER
45 # Types needed only for Type Hints
46 from google.cloud.firestore_v1.base_document import DocumentSnapshot
47 from google.cloud.firestore_v1.transaction import Transaction
48 from google.cloud.firestore_v1.field_path import FieldPath
50_AUTO_ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
53class BaseCollectionReference(Generic[QueryType]):
54 """A reference to a collection in a Firestore database.
56 The collection may already exist or this class can facilitate creation
57 of documents within the collection.
59 Args:
60 path (Tuple[str, ...]): The components in the collection path.
61 This is a series of strings representing each collection and
62 sub-collection ID, as well as the document IDs for any documents
63 that contain a sub-collection.
64 kwargs (dict): The keyword arguments for the constructor. The only
65 supported keyword is ``client`` and it must be a
66 :class:`~google.cloud.firestore_v1.client.Client` if provided. It
67 represents the client that created this collection reference.
69 Raises:
70 ValueError: if
72 * the ``path`` is empty
73 * there are an even number of elements
74 * a collection ID in ``path`` is not a string
75 * a document ID in ``path`` is not a string
76 TypeError: If a keyword other than ``client`` is used.
77 """
79 def __init__(self, *path, **kwargs) -> None:
80 _helpers.verify_path(path, is_collection=True)
81 self._path = path
82 self._client = kwargs.pop("client", None)
83 if kwargs:
84 raise TypeError(
85 "Received unexpected arguments", kwargs, "Only `client` is supported"
86 )
88 def __eq__(self, other):
89 if not isinstance(other, self.__class__):
90 return NotImplemented
91 return self._path == other._path and self._client == other._client
93 @property
94 def id(self):
95 """The collection identifier.
97 Returns:
98 str: The last component of the path.
99 """
100 return self._path[-1]
102 @property
103 def parent(self):
104 """Document that owns the current collection.
106 Returns:
107 Optional[:class:`~google.cloud.firestore_v1.document.DocumentReference`]:
108 The parent document, if the current collection is not a
109 top-level collection.
110 """
111 if len(self._path) == 1:
112 return None
113 else:
114 parent_path = self._path[:-1]
115 return self._client.document(*parent_path)
117 def _query(self) -> QueryType:
118 raise NotImplementedError
120 def _aggregation_query(self) -> BaseAggregationQuery:
121 raise NotImplementedError
123 def document(self, document_id: Optional[str] = None) -> DocumentReference:
124 """Create a sub-document underneath the current collection.
126 Args:
127 document_id (Optional[str]): The document identifier
128 within the current collection. If not provided, will default
129 to a random 20 character string composed of digits,
130 uppercase and lowercase and letters.
132 Returns:
133 :class:`~google.cloud.firestore_v1.document.DocumentReference`:
134 The child document.
135 """
136 if document_id is None:
137 document_id = _auto_id()
139 # Append `self._path` and the passed document's ID as long as the first
140 # element in the path is not an empty string, which comes from setting the
141 # parent to "" for recursive queries.
142 child_path = self._path + (document_id,) if self._path[0] else (document_id,)
143 return self._client.document(*child_path)
145 def _parent_info(self) -> Tuple[Any, str]:
146 """Get fully-qualified parent path and prefix for this collection.
148 Returns:
149 Tuple[str, str]: Pair of
151 * the fully-qualified (with database and project) path to the
152 parent of this collection (will either be the database path
153 or a document path).
154 * the prefix to a document in this collection.
155 """
156 parent_doc = self.parent
157 if parent_doc is None:
158 parent_path = _helpers.DOCUMENT_PATH_DELIMITER.join(
159 (self._client._database_string, "documents")
160 )
161 else:
162 parent_path = parent_doc._document_path
164 expected_prefix = _helpers.DOCUMENT_PATH_DELIMITER.join((parent_path, self.id))
165 return parent_path, expected_prefix
167 def _prep_add(
168 self,
169 document_data: dict,
170 document_id: Optional[str] = None,
171 retry: Optional[retries.Retry] = None,
172 timeout: Optional[float] = None,
173 ) -> Tuple[DocumentReference, dict]:
174 """Shared setup for async / sync :method:`add`"""
175 if document_id is None:
176 document_id = _auto_id()
178 document_ref = self.document(document_id)
179 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout)
181 return document_ref, kwargs
183 def add(
184 self,
185 document_data: dict,
186 document_id: Optional[str] = None,
187 retry: Optional[retries.Retry] = None,
188 timeout: Optional[float] = None,
189 ) -> Union[Tuple[Any, Any], Coroutine[Any, Any, Tuple[Any, Any]]]:
190 raise NotImplementedError
192 def _prep_list_documents(
193 self,
194 page_size: Optional[int] = None,
195 retry: Optional[retries.Retry] = None,
196 timeout: Optional[float] = None,
197 ) -> Tuple[dict, dict]:
198 """Shared setup for async / sync :method:`list_documents`"""
199 parent, _ = self._parent_info()
200 request = {
201 "parent": parent,
202 "collection_id": self.id,
203 "page_size": page_size,
204 "show_missing": True,
205 # list_documents returns an iterator of document references, which do not
206 # include any fields. To save on data transfer, we can set a field_path mask
207 # to include no fields
208 "mask": {"field_paths": None},
209 }
210 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout)
212 return request, kwargs
214 def list_documents(
215 self,
216 page_size: Optional[int] = None,
217 retry: Optional[retries.Retry] = None,
218 timeout: Optional[float] = None,
219 ) -> Union[
220 Generator[DocumentReference, Any, Any], AsyncGenerator[DocumentReference, Any]
221 ]:
222 raise NotImplementedError
224 def recursive(self) -> QueryType:
225 return self._query().recursive()
227 def select(self, field_paths: Iterable[str]) -> QueryType:
228 """Create a "select" query with this collection as parent.
230 See
231 :meth:`~google.cloud.firestore_v1.query.Query.select` for
232 more information on this method.
234 Args:
235 field_paths (Iterable[str, ...]): An iterable of field paths
236 (``.``-delimited list of field names) to use as a projection
237 of document fields in the query results.
239 Returns:
240 :class:`~google.cloud.firestore_v1.query.Query`:
241 A "projected" query.
242 """
243 query = self._query()
244 return query.select(field_paths)
246 def where(
247 self,
248 field_path: Optional[str] = None,
249 op_string: Optional[str] = None,
250 value=None,
251 *,
252 filter=None,
253 ) -> QueryType:
254 """Create a "where" query with this collection as parent.
256 See
257 :meth:`~google.cloud.firestore_v1.query.Query.where` for
258 more information on this method.
260 Args:
261 field_path (str): A field path (``.``-delimited list of
262 field names) for the field to filter on. Optional.
263 op_string (str): A comparison operation in the form of a string.
264 Acceptable values are ``<``, ``<=``, ``==``, ``>=``, ``>``,
265 and ``in``. Optional.
266 value (Any): The value to compare the field against in the filter.
267 If ``value`` is :data:`None` or a NaN, then ``==`` is the only
268 allowed operation. If ``op_string`` is ``in``, ``value``
269 must be a sequence of values. Optional.
270 filter (class:`~google.cloud.firestore_v1.base_query.BaseFilter`): an instance of a Filter.
271 Either a FieldFilter or a CompositeFilter.
272 Returns:
273 :class:`~google.cloud.firestore_v1.query.Query`:
274 A filtered query.
275 Raises:
276 ValueError, if both the positional arguments (field_path, op_string, value)
277 and the filter keyword argument are passed at the same time.
278 """
279 query = self._query()
280 if field_path and op_string:
281 if filter is not None:
282 raise ValueError(
283 "Can't pass in both the positional arguments and 'filter' at the same time"
284 )
285 if field_path == "__name__" and op_string == "in":
286 wrapped_names = []
288 for name in value:
289 if isinstance(name, str):
290 name = self.document(name)
292 wrapped_names.append(name)
294 value = wrapped_names
295 return query.where(field_path, op_string, value)
296 else:
297 return query.where(filter=filter)
299 def order_by(self, field_path: str, **kwargs) -> QueryType:
300 """Create an "order by" query with this collection as parent.
302 See
303 :meth:`~google.cloud.firestore_v1.query.Query.order_by` for
304 more information on this method.
306 Args:
307 field_path (str): A field path (``.``-delimited list of
308 field names) on which to order the query results.
309 kwargs (Dict[str, Any]): The keyword arguments to pass along
310 to the query. The only supported keyword is ``direction``,
311 see :meth:`~google.cloud.firestore_v1.query.Query.order_by`
312 for more information.
314 Returns:
315 :class:`~google.cloud.firestore_v1.query.Query`:
316 An "order by" query.
317 """
318 query = self._query()
319 return query.order_by(field_path, **kwargs)
321 def limit(self, count: int) -> QueryType:
322 """Create a limited query with this collection as parent.
324 .. note::
325 `limit` and `limit_to_last` are mutually exclusive.
326 Setting `limit` will drop previously set `limit_to_last`.
328 See
329 :meth:`~google.cloud.firestore_v1.query.Query.limit` for
330 more information on this method.
332 Args:
333 count (int): Maximum number of documents to return that match
334 the query.
336 Returns:
337 :class:`~google.cloud.firestore_v1.query.Query`:
338 A limited query.
339 """
340 query = self._query()
341 return query.limit(count)
343 def limit_to_last(self, count: int):
344 """Create a limited to last query with this collection as parent.
346 .. note::
347 `limit` and `limit_to_last` are mutually exclusive.
348 Setting `limit_to_last` will drop previously set `limit`.
350 See
351 :meth:`~google.cloud.firestore_v1.query.Query.limit_to_last`
352 for more information on this method.
354 Args:
355 count (int): Maximum number of documents to return that
356 match the query.
357 Returns:
358 :class:`~google.cloud.firestore_v1.query.Query`:
359 A limited to last query.
360 """
361 query = self._query()
362 return query.limit_to_last(count)
364 def offset(self, num_to_skip: int) -> QueryType:
365 """Skip to an offset in a query with this collection as parent.
367 See
368 :meth:`~google.cloud.firestore_v1.query.Query.offset` for
369 more information on this method.
371 Args:
372 num_to_skip (int): The number of results to skip at the beginning
373 of query results. (Must be non-negative.)
375 Returns:
376 :class:`~google.cloud.firestore_v1.query.Query`:
377 An offset query.
378 """
379 query = self._query()
380 return query.offset(num_to_skip)
382 def start_at(
383 self, document_fields: Union[DocumentSnapshot, dict, list, tuple]
384 ) -> QueryType:
385 """Start query at a cursor with this collection as parent.
387 See
388 :meth:`~google.cloud.firestore_v1.query.Query.start_at` for
389 more information on this method.
391 Args:
392 document_fields (Union[:class:`~google.cloud.firestore_v1.\
393 document.DocumentSnapshot`, dict, list, tuple]):
394 A document snapshot or a dictionary/list/tuple of fields
395 representing a query results cursor. A cursor is a collection
396 of values that represent a position in a query result set.
398 Returns:
399 :class:`~google.cloud.firestore_v1.query.Query`:
400 A query with cursor.
401 """
402 query = self._query()
403 return query.start_at(document_fields)
405 def start_after(
406 self, document_fields: Union[DocumentSnapshot, dict, list, tuple]
407 ) -> QueryType:
408 """Start query after a cursor with this collection as parent.
410 See
411 :meth:`~google.cloud.firestore_v1.query.Query.start_after` for
412 more information on this method.
414 Args:
415 document_fields (Union[:class:`~google.cloud.firestore_v1.\
416 document.DocumentSnapshot`, dict, list, tuple]):
417 A document snapshot or a dictionary/list/tuple of fields
418 representing a query results cursor. A cursor is a collection
419 of values that represent a position in a query result set.
421 Returns:
422 :class:`~google.cloud.firestore_v1.query.Query`:
423 A query with cursor.
424 """
425 query = self._query()
426 return query.start_after(document_fields)
428 def end_before(
429 self, document_fields: Union[DocumentSnapshot, dict, list, tuple]
430 ) -> QueryType:
431 """End query before a cursor with this collection as parent.
433 See
434 :meth:`~google.cloud.firestore_v1.query.Query.end_before` for
435 more information on this method.
437 Args:
438 document_fields (Union[:class:`~google.cloud.firestore_v1.\
439 document.DocumentSnapshot`, dict, list, tuple]):
440 A document snapshot or a dictionary/list/tuple of fields
441 representing a query results cursor. A cursor is a collection
442 of values that represent a position in a query result set.
444 Returns:
445 :class:`~google.cloud.firestore_v1.query.Query`:
446 A query with cursor.
447 """
448 query = self._query()
449 return query.end_before(document_fields)
451 def end_at(
452 self, document_fields: Union[DocumentSnapshot, dict, list, tuple]
453 ) -> QueryType:
454 """End query at a cursor with this collection as parent.
456 See
457 :meth:`~google.cloud.firestore_v1.query.Query.end_at` for
458 more information on this method.
460 Args:
461 document_fields (Union[:class:`~google.cloud.firestore_v1.\
462 document.DocumentSnapshot`, dict, list, tuple]):
463 A document snapshot or a dictionary/list/tuple of fields
464 representing a query results cursor. A cursor is a collection
465 of values that represent a position in a query result set.
467 Returns:
468 :class:`~google.cloud.firestore_v1.query.Query`:
469 A query with cursor.
470 """
471 query = self._query()
472 return query.end_at(document_fields)
474 def _prep_get_or_stream(
475 self,
476 retry: Optional[retries.Retry] = None,
477 timeout: Optional[float] = None,
478 ) -> Tuple[Any, dict]:
479 """Shared setup for async / sync :meth:`get` / :meth:`stream`"""
480 query = self._query()
481 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout)
483 return query, kwargs
485 def get(
486 self,
487 transaction: Optional[Transaction] = None,
488 retry: Optional[retries.Retry] = None,
489 timeout: Optional[float] = None,
490 ) -> Union[
491 Generator[DocumentSnapshot, Any, Any], AsyncGenerator[DocumentSnapshot, Any]
492 ]:
493 raise NotImplementedError
495 def stream(
496 self,
497 transaction: Optional[Transaction] = None,
498 retry: Optional[retries.Retry] = None,
499 timeout: Optional[float] = None,
500 ) -> Union[Iterator[DocumentSnapshot], AsyncIterator[DocumentSnapshot]]:
501 raise NotImplementedError
503 def on_snapshot(self, callback) -> NoReturn:
504 raise NotImplementedError
506 def count(self, alias=None):
507 """
508 Adds a count over the nested query.
510 :type alias: str
511 :param alias: (Optional) The alias for the count
512 """
513 return self._aggregation_query().count(alias=alias)
515 def sum(self, field_ref: str | FieldPath, alias=None):
516 """
517 Adds a sum over the nested query.
519 :type field_ref: Union[str, google.cloud.firestore_v1.field_path.FieldPath]
520 :param field_ref: The field to aggregate across.
522 :type alias: Optional[str]
523 :param alias: Optional name of the field to store the result of the aggregation into.
524 If not provided, Firestore will pick a default name following the format field_<incremental_id++>.
526 """
527 return self._aggregation_query().sum(field_ref, alias=alias)
529 def avg(self, field_ref: str | FieldPath, alias=None):
530 """
531 Adds an avg over the nested query.
533 :type field_ref: Union[str, google.cloud.firestore_v1.field_path.FieldPath]
534 :param field_ref: The field to aggregate across.
536 :type alias: Optional[str]
537 :param alias: Optional name of the field to store the result of the aggregation into.
538 If not provided, Firestore will pick a default name following the format field_<incremental_id++>.
539 """
540 return self._aggregation_query().avg(field_ref, alias=alias)
543def _auto_id() -> str:
544 """Generate a "random" automatically generated ID.
546 Returns:
547 str: A 20 character string composed of digits, uppercase and
548 lowercase and letters.
549 """
551 return "".join(random.choice(_AUTO_ID_CHARS) for _ in range(20))
554def _item_to_document_ref(collection_reference, item) -> DocumentReference:
555 """Convert Document resource to document ref.
557 Args:
558 collection_reference (google.api_core.page_iterator.GRPCIterator):
559 iterator response
560 item (dict): document resource
561 """
562 document_id = item.name.split(_helpers.DOCUMENT_PATH_DELIMITER)[-1]
563 return collection_reference.document(document_id)