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

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 collections for the Google Cloud Firestore API.""" 

16from __future__ import annotations 

17import random 

18 

19from google.api_core import retry as retries 

20 

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 

25 

26 

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) 

42 

43 

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 

49 

50_AUTO_ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" 

51 

52 

53class BaseCollectionReference(Generic[QueryType]): 

54 """A reference to a collection in a Firestore database. 

55 

56 The collection may already exist or this class can facilitate creation 

57 of documents within the collection. 

58 

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. 

68 

69 Raises: 

70 ValueError: if 

71 

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 """ 

78 

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 ) 

87 

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 

92 

93 @property 

94 def id(self): 

95 """The collection identifier. 

96 

97 Returns: 

98 str: The last component of the path. 

99 """ 

100 return self._path[-1] 

101 

102 @property 

103 def parent(self): 

104 """Document that owns the current collection. 

105 

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) 

116 

117 def _query(self) -> QueryType: 

118 raise NotImplementedError 

119 

120 def _aggregation_query(self) -> BaseAggregationQuery: 

121 raise NotImplementedError 

122 

123 def document(self, document_id: Optional[str] = None) -> DocumentReference: 

124 """Create a sub-document underneath the current collection. 

125 

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. 

131 

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() 

138 

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) 

144 

145 def _parent_info(self) -> Tuple[Any, str]: 

146 """Get fully-qualified parent path and prefix for this collection. 

147 

148 Returns: 

149 Tuple[str, str]: Pair of 

150 

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 

163 

164 expected_prefix = _helpers.DOCUMENT_PATH_DELIMITER.join((parent_path, self.id)) 

165 return parent_path, expected_prefix 

166 

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() 

177 

178 document_ref = self.document(document_id) 

179 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

180 

181 return document_ref, kwargs 

182 

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 

191 

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) 

211 

212 return request, kwargs 

213 

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 

223 

224 def recursive(self) -> QueryType: 

225 return self._query().recursive() 

226 

227 def select(self, field_paths: Iterable[str]) -> QueryType: 

228 """Create a "select" query with this collection as parent. 

229 

230 See 

231 :meth:`~google.cloud.firestore_v1.query.Query.select` for 

232 more information on this method. 

233 

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. 

238 

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) 

245 

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. 

255 

256 See 

257 :meth:`~google.cloud.firestore_v1.query.Query.where` for 

258 more information on this method. 

259 

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 = [] 

287 

288 for name in value: 

289 if isinstance(name, str): 

290 name = self.document(name) 

291 

292 wrapped_names.append(name) 

293 

294 value = wrapped_names 

295 return query.where(field_path, op_string, value) 

296 else: 

297 return query.where(filter=filter) 

298 

299 def order_by(self, field_path: str, **kwargs) -> QueryType: 

300 """Create an "order by" query with this collection as parent. 

301 

302 See 

303 :meth:`~google.cloud.firestore_v1.query.Query.order_by` for 

304 more information on this method. 

305 

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. 

313 

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) 

320 

321 def limit(self, count: int) -> QueryType: 

322 """Create a limited query with this collection as parent. 

323 

324 .. note:: 

325 `limit` and `limit_to_last` are mutually exclusive. 

326 Setting `limit` will drop previously set `limit_to_last`. 

327 

328 See 

329 :meth:`~google.cloud.firestore_v1.query.Query.limit` for 

330 more information on this method. 

331 

332 Args: 

333 count (int): Maximum number of documents to return that match 

334 the query. 

335 

336 Returns: 

337 :class:`~google.cloud.firestore_v1.query.Query`: 

338 A limited query. 

339 """ 

340 query = self._query() 

341 return query.limit(count) 

342 

343 def limit_to_last(self, count: int): 

344 """Create a limited to last query with this collection as parent. 

345 

346 .. note:: 

347 `limit` and `limit_to_last` are mutually exclusive. 

348 Setting `limit_to_last` will drop previously set `limit`. 

349 

350 See 

351 :meth:`~google.cloud.firestore_v1.query.Query.limit_to_last` 

352 for more information on this method. 

353 

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) 

363 

364 def offset(self, num_to_skip: int) -> QueryType: 

365 """Skip to an offset in a query with this collection as parent. 

366 

367 See 

368 :meth:`~google.cloud.firestore_v1.query.Query.offset` for 

369 more information on this method. 

370 

371 Args: 

372 num_to_skip (int): The number of results to skip at the beginning 

373 of query results. (Must be non-negative.) 

374 

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) 

381 

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. 

386 

387 See 

388 :meth:`~google.cloud.firestore_v1.query.Query.start_at` for 

389 more information on this method. 

390 

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. 

397 

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) 

404 

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. 

409 

410 See 

411 :meth:`~google.cloud.firestore_v1.query.Query.start_after` for 

412 more information on this method. 

413 

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. 

420 

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) 

427 

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. 

432 

433 See 

434 :meth:`~google.cloud.firestore_v1.query.Query.end_before` for 

435 more information on this method. 

436 

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. 

443 

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) 

450 

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. 

455 

456 See 

457 :meth:`~google.cloud.firestore_v1.query.Query.end_at` for 

458 more information on this method. 

459 

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. 

466 

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) 

473 

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) 

482 

483 return query, kwargs 

484 

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 

494 

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 

502 

503 def on_snapshot(self, callback) -> NoReturn: 

504 raise NotImplementedError 

505 

506 def count(self, alias=None): 

507 """ 

508 Adds a count over the nested query. 

509 

510 :type alias: str 

511 :param alias: (Optional) The alias for the count 

512 """ 

513 return self._aggregation_query().count(alias=alias) 

514 

515 def sum(self, field_ref: str | FieldPath, alias=None): 

516 """ 

517 Adds a sum over the nested query. 

518 

519 :type field_ref: Union[str, google.cloud.firestore_v1.field_path.FieldPath] 

520 :param field_ref: The field to aggregate across. 

521 

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++>. 

525 

526 """ 

527 return self._aggregation_query().sum(field_ref, alias=alias) 

528 

529 def avg(self, field_ref: str | FieldPath, alias=None): 

530 """ 

531 Adds an avg over the nested query. 

532 

533 :type field_ref: Union[str, google.cloud.firestore_v1.field_path.FieldPath] 

534 :param field_ref: The field to aggregate across. 

535 

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) 

541 

542 

543def _auto_id() -> str: 

544 """Generate a "random" automatically generated ID. 

545 

546 Returns: 

547 str: A 20 character string composed of digits, uppercase and 

548 lowercase and letters. 

549 """ 

550 

551 return "".join(random.choice(_AUTO_ID_CHARS) for _ in range(20)) 

552 

553 

554def _item_to_document_ref(collection_reference, item) -> DocumentReference: 

555 """Convert Document resource to document ref. 

556 

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)