Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/google/cloud/firestore_v1/base_document.py: 37%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

153 statements  

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 

17 

18import copy 

19 

20from typing import ( 

21 TYPE_CHECKING, 

22 Any, 

23 Dict, 

24 Iterable, 

25 Optional, 

26 Tuple, 

27 Union, 

28 Awaitable, 

29) 

30 

31from google.api_core import retry as retries 

32 

33from google.cloud.firestore_v1 import _helpers 

34from google.cloud.firestore_v1 import field_path as field_path_module 

35from google.cloud.firestore_v1.types import common 

36 

37# Types needed only for Type Hints 

38if TYPE_CHECKING: # pragma: NO COVER 

39 from google.cloud.firestore_v1.types import Document, firestore, write 

40 

41 import datetime 

42 

43 

44class BaseDocumentReference(object): 

45 """A reference to a document in a Firestore database. 

46 

47 The document may already exist or can be created by this class. 

48 

49 Args: 

50 path (Tuple[str, ...]): The components in the document path. 

51 This is a series of strings representing each collection and 

52 sub-collection ID, as well as the document IDs for any documents 

53 that contain a sub-collection (as well as the base document). 

54 kwargs (dict): The keyword arguments for the constructor. The only 

55 supported keyword is ``client`` and it must be a 

56 :class:`~google.cloud.firestore_v1.client.Client`. It represents 

57 the client that created this document reference. 

58 

59 Raises: 

60 ValueError: if 

61 

62 * the ``path`` is empty 

63 * there are an even number of elements 

64 * a collection ID in ``path`` is not a string 

65 * a document ID in ``path`` is not a string 

66 TypeError: If a keyword other than ``client`` is used. 

67 """ 

68 

69 _document_path_internal = None 

70 

71 def __init__(self, *path, **kwargs) -> None: 

72 _helpers.verify_path(path, is_collection=False) 

73 self._path = path 

74 self._client = kwargs.pop("client", None) 

75 if kwargs: 

76 raise TypeError( 

77 "Received unexpected arguments", kwargs, "Only `client` is supported" 

78 ) 

79 

80 def __copy__(self): 

81 """Shallow copy the instance. 

82 

83 We leave the client "as-is" but tuple-unpack the path. 

84 

85 Returns: 

86 .DocumentReference: A copy of the current document. 

87 """ 

88 result = self.__class__(*self._path, client=self._client) 

89 result._document_path_internal = self._document_path_internal 

90 return result 

91 

92 def __deepcopy__(self, unused_memo): 

93 """Deep copy the instance. 

94 

95 This isn't a true deep copy, wee leave the client "as-is" but 

96 tuple-unpack the path. 

97 

98 Returns: 

99 .DocumentReference: A copy of the current document. 

100 """ 

101 return self.__copy__() 

102 

103 def __eq__(self, other): 

104 """Equality check against another instance. 

105 

106 Args: 

107 other (Any): A value to compare against. 

108 

109 Returns: 

110 Union[bool, NotImplementedType]: Indicating if the values are 

111 equal. 

112 """ 

113 if isinstance(other, self.__class__): 

114 return self._client == other._client and self._path == other._path 

115 else: 

116 return NotImplemented 

117 

118 def __hash__(self): 

119 return hash(self._path) + hash(self._client) 

120 

121 def __ne__(self, other): 

122 """Inequality check against another instance. 

123 

124 Args: 

125 other (Any): A value to compare against. 

126 

127 Returns: 

128 Union[bool, NotImplementedType]: Indicating if the values are 

129 not equal. 

130 """ 

131 if isinstance(other, self.__class__): 

132 return self._client != other._client or self._path != other._path 

133 else: 

134 return NotImplemented 

135 

136 @property 

137 def path(self): 

138 """Database-relative for this document. 

139 

140 Returns: 

141 str: The document's relative path. 

142 """ 

143 return "/".join(self._path) 

144 

145 @property 

146 def _document_path(self): 

147 """Create and cache the full path for this document. 

148 

149 Of the form: 

150 

151 ``projects/{project_id}/databases/{database_id}/... 

152 documents/{document_path}`` 

153 

154 Returns: 

155 str: The full document path. 

156 

157 Raises: 

158 ValueError: If the current document reference has no ``client``. 

159 """ 

160 if self._document_path_internal is None: 

161 if self._client is None: 

162 raise ValueError("A document reference requires a `client`.") 

163 self._document_path_internal = _get_document_path(self._client, self._path) 

164 

165 return self._document_path_internal 

166 

167 @property 

168 def id(self): 

169 """The document identifier (within its collection). 

170 

171 Returns: 

172 str: The last component of the path. 

173 """ 

174 return self._path[-1] 

175 

176 @property 

177 def parent(self): 

178 """Collection that owns the current document. 

179 

180 Returns: 

181 :class:`~google.cloud.firestore_v1.collection.CollectionReference`: 

182 The parent collection. 

183 """ 

184 parent_path = self._path[:-1] 

185 return self._client.collection(*parent_path) 

186 

187 def collection(self, collection_id: str): 

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

189 

190 Args: 

191 collection_id (str): The sub-collection identifier (sometimes 

192 referred to as the "kind"). 

193 

194 Returns: 

195 :class:`~google.cloud.firestore_v1.collection.CollectionReference`: 

196 The child collection. 

197 """ 

198 child_path = self._path + (collection_id,) 

199 return self._client.collection(*child_path) 

200 

201 def _prep_create( 

202 self, 

203 document_data: dict, 

204 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

205 timeout: float | None = None, 

206 ) -> Tuple[Any, dict]: 

207 batch = self._client.batch() 

208 batch.create(self, document_data) 

209 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

210 

211 return batch, kwargs 

212 

213 def create( 

214 self, 

215 document_data: dict, 

216 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

217 timeout: float | None = None, 

218 ) -> write.WriteResult | Awaitable[write.WriteResult]: 

219 raise NotImplementedError 

220 

221 def _prep_set( 

222 self, 

223 document_data: dict, 

224 merge: bool = False, 

225 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

226 timeout: float | None = None, 

227 ) -> Tuple[Any, dict]: 

228 batch = self._client.batch() 

229 batch.set(self, document_data, merge=merge) 

230 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

231 

232 return batch, kwargs 

233 

234 def set( 

235 self, 

236 document_data: dict, 

237 merge: bool = False, 

238 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

239 timeout: float | None = None, 

240 ): 

241 raise NotImplementedError 

242 

243 def _prep_update( 

244 self, 

245 field_updates: dict, 

246 option: _helpers.WriteOption | None = None, 

247 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

248 timeout: float | None = None, 

249 ) -> Tuple[Any, dict]: 

250 batch = self._client.batch() 

251 batch.update(self, field_updates, option=option) 

252 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

253 

254 return batch, kwargs 

255 

256 def update( 

257 self, 

258 field_updates: dict, 

259 option: _helpers.WriteOption | None = None, 

260 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

261 timeout: float | None = None, 

262 ): 

263 raise NotImplementedError 

264 

265 def _prep_delete( 

266 self, 

267 option: _helpers.WriteOption | None = None, 

268 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

269 timeout: float | None = None, 

270 ) -> Tuple[dict, dict]: 

271 """Shared setup for async/sync :meth:`delete`.""" 

272 write_pb = _helpers.pb_for_delete(self._document_path, option) 

273 request = { 

274 "database": self._client._database_string, 

275 "writes": [write_pb], 

276 "transaction": None, 

277 } 

278 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

279 

280 return request, kwargs 

281 

282 def delete( 

283 self, 

284 option: _helpers.WriteOption | None = None, 

285 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

286 timeout: float | None = None, 

287 ): 

288 raise NotImplementedError 

289 

290 def _prep_batch_get( 

291 self, 

292 field_paths: Iterable[str] | None = None, 

293 transaction=None, 

294 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

295 timeout: float | None = None, 

296 read_time: datetime.datetime | None = None, 

297 ) -> Tuple[dict, dict]: 

298 """Shared setup for async/sync :meth:`get`.""" 

299 if isinstance(field_paths, str): 

300 raise ValueError("'field_paths' must be a sequence of paths, not a string.") 

301 

302 if field_paths is not None: 

303 mask = common.DocumentMask(field_paths=sorted(field_paths)) 

304 else: 

305 mask = None 

306 

307 request = { 

308 "database": self._client._database_string, 

309 "documents": [self._document_path], 

310 "mask": mask, 

311 "transaction": _helpers.get_transaction_id(transaction), 

312 } 

313 if read_time is not None: 

314 request["read_time"] = read_time 

315 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

316 

317 return request, kwargs 

318 

319 def get( 

320 self, 

321 field_paths: Iterable[str] | None = None, 

322 transaction=None, 

323 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

324 timeout: float | None = None, 

325 *, 

326 read_time: datetime.datetime | None = None, 

327 ) -> "DocumentSnapshot" | Awaitable["DocumentSnapshot"]: 

328 raise NotImplementedError 

329 

330 def _prep_collections( 

331 self, 

332 page_size: int | None = None, 

333 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

334 timeout: float | None = None, 

335 read_time: datetime.datetime | None = None, 

336 ) -> Tuple[dict, dict]: 

337 """Shared setup for async/sync :meth:`collections`.""" 

338 request = { 

339 "parent": self._document_path, 

340 "page_size": page_size, 

341 } 

342 if read_time is not None: 

343 request["read_time"] = read_time 

344 kwargs = _helpers.make_retry_timeout_kwargs(retry, timeout) 

345 

346 return request, kwargs 

347 

348 def collections( 

349 self, 

350 page_size: int | None = None, 

351 retry: retries.Retry | retries.AsyncRetry | None | object = None, 

352 timeout: float | None = None, 

353 *, 

354 read_time: datetime.datetime | None = None, 

355 ): 

356 raise NotImplementedError 

357 

358 def on_snapshot(self, callback): 

359 raise NotImplementedError 

360 

361 

362class DocumentSnapshot(object): 

363 """A snapshot of document data in a Firestore database. 

364 

365 This represents data retrieved at a specific time and may not contain 

366 all fields stored for the document (i.e. a hand-picked selection of 

367 fields may have been retrieved). 

368 

369 Instances of this class are not intended to be constructed by hand, 

370 rather they'll be returned as responses to various methods, such as 

371 :meth:`~google.cloud.DocumentReference.get`. 

372 

373 Args: 

374 reference (:class:`~google.cloud.firestore_v1.document.DocumentReference`): 

375 A document reference corresponding to the document that contains 

376 the data in this snapshot. 

377 data (Dict[str, Any]): 

378 The data retrieved in the snapshot. 

379 exists (bool): 

380 Indicates if the document existed at the time the snapshot was 

381 retrieved. 

382 read_time (:class:`proto.datetime_helpers.DatetimeWithNanoseconds`): 

383 The time that this snapshot was read from the server. 

384 create_time (:class:`proto.datetime_helpers.DatetimeWithNanoseconds`): 

385 The time that this document was created. 

386 update_time (:class:`proto.datetime_helpers.DatetimeWithNanoseconds`): 

387 The time that this document was last updated. 

388 """ 

389 

390 def __init__( 

391 self, reference, data, exists, read_time, create_time, update_time 

392 ) -> None: 

393 self._reference = reference 

394 # We want immutable data, so callers can't modify this value 

395 # out from under us. 

396 self._data = copy.deepcopy(data) 

397 self._exists = exists 

398 self.read_time = read_time 

399 self.create_time = create_time 

400 self.update_time = update_time 

401 

402 def __eq__(self, other): 

403 if not isinstance(other, self.__class__): 

404 return NotImplemented 

405 return self._reference == other._reference and self._data == other._data 

406 

407 def __hash__(self): 

408 return hash(self._reference) + hash(self.update_time) 

409 

410 @property 

411 def _client(self): 

412 """The client that owns the document reference for this snapshot. 

413 

414 Returns: 

415 :class:`~google.cloud.firestore_v1.client.Client`: 

416 The client that owns this document. 

417 """ 

418 return self._reference._client 

419 

420 @property 

421 def exists(self): 

422 """Existence flag. 

423 

424 Indicates if the document existed at the time this snapshot 

425 was retrieved. 

426 

427 Returns: 

428 bool: The existence flag. 

429 """ 

430 return self._exists 

431 

432 @property 

433 def id(self): 

434 """The document identifier (within its collection). 

435 

436 Returns: 

437 str: The last component of the path of the document. 

438 """ 

439 return self._reference.id 

440 

441 @property 

442 def reference(self): 

443 """Document reference corresponding to document that owns this data. 

444 

445 Returns: 

446 :class:`~google.cloud.firestore_v1.document.DocumentReference`: 

447 A document reference corresponding to this document. 

448 """ 

449 return self._reference 

450 

451 def get(self, field_path: str) -> Any: 

452 """Get a value from the snapshot data. 

453 

454 If the data is nested, for example: 

455 

456 .. code-block:: python 

457 

458 >>> snapshot.to_dict() 

459 { 

460 'top1': { 

461 'middle2': { 

462 'bottom3': 20, 

463 'bottom4': 22, 

464 }, 

465 'middle5': True, 

466 }, 

467 'top6': b'\x00\x01 foo', 

468 } 

469 

470 a **field path** can be used to access the nested data. For 

471 example: 

472 

473 .. code-block:: python 

474 

475 >>> snapshot.get('top1') 

476 { 

477 'middle2': { 

478 'bottom3': 20, 

479 'bottom4': 22, 

480 }, 

481 'middle5': True, 

482 } 

483 >>> snapshot.get('top1.middle2') 

484 { 

485 'bottom3': 20, 

486 'bottom4': 22, 

487 } 

488 >>> snapshot.get('top1.middle2.bottom3') 

489 20 

490 

491 See :meth:`~google.cloud.firestore_v1.client.Client.field_path` for 

492 more information on **field paths**. 

493 

494 A copy is returned since the data may contain mutable values, 

495 but the data stored in the snapshot must remain immutable. 

496 

497 Args: 

498 field_path (str): A field path (``.``-delimited list of 

499 field names). 

500 

501 Returns: 

502 Any or None: 

503 (A copy of) the value stored for the ``field_path`` or 

504 None if snapshot document does not exist. 

505 

506 Raises: 

507 KeyError: If the ``field_path`` does not match nested data 

508 in the snapshot. 

509 """ 

510 if not self._exists: 

511 return None 

512 nested_data = field_path_module.get_nested_value(field_path, self._data) 

513 return copy.deepcopy(nested_data) 

514 

515 def to_dict(self) -> Union[Dict[str, Any], None]: 

516 """Retrieve the data contained in this snapshot. 

517 

518 A copy is returned since the data may contain mutable values, 

519 but the data stored in the snapshot must remain immutable. 

520 

521 Returns: 

522 Dict[str, Any] or None: 

523 The data in the snapshot. Returns None if reference 

524 does not exist. 

525 """ 

526 if not self._exists: 

527 return None 

528 return copy.deepcopy(self._data) 

529 

530 def _to_protobuf(self) -> Optional[Document]: 

531 return _helpers.document_snapshot_to_protobuf(self) 

532 

533 

534def _get_document_path(client, path: Tuple[str]) -> str: 

535 """Convert a path tuple into a full path string. 

536 

537 Of the form: 

538 

539 ``projects/{project_id}/databases/{database_id}/... 

540 documents/{document_path}`` 

541 

542 Args: 

543 client (:class:`~google.cloud.firestore_v1.client.Client`): 

544 The client that holds configuration details and a GAPIC client 

545 object. 

546 path (Tuple[str, ...]): The components in a document path. 

547 

548 Returns: 

549 str: The fully-qualified document path. 

550 """ 

551 parts = (client._database_string, "documents") + path 

552 return _helpers.DOCUMENT_PATH_DELIMITER.join(parts) 

553 

554 

555def _consume_single_get(response_iterator) -> firestore.BatchGetDocumentsResponse: 

556 """Consume a gRPC stream that should contain a single response. 

557 

558 The stream will correspond to a ``BatchGetDocuments`` request made 

559 for a single document. 

560 

561 Args: 

562 response_iterator (~google.cloud.exceptions.GrpcRendezvous): A 

563 streaming iterator returned from a ``BatchGetDocuments`` 

564 request. 

565 

566 Returns: 

567 ~google.cloud.firestore_v1.\ 

568 firestore.BatchGetDocumentsResponse: The single "get" 

569 response in the batch. 

570 

571 Raises: 

572 ValueError: If anything other than exactly one response is returned. 

573 """ 

574 # Calling ``list()`` consumes the entire iterator. 

575 all_responses = list(response_iterator) 

576 if len(all_responses) != 1: 

577 raise ValueError( 

578 "Unexpected response from `BatchGetDocumentsResponse`", 

579 all_responses, 

580 "Expected only one result", 

581 ) 

582 

583 return all_responses[0] 

584 

585 

586def _first_write_result(write_results: list) -> write.WriteResult: 

587 """Get first write result from list. 

588 

589 For cases where ``len(write_results) > 1``, this assumes the writes 

590 occurred at the same time (e.g. if an update and transform are sent 

591 at the same time). 

592 

593 Args: 

594 write_results (List[google.cloud.firestore_v1.\ 

595 write.WriteResult, ...]: The write results from a 

596 ``CommitResponse``. 

597 

598 Returns: 

599 google.cloud.firestore_v1.types.WriteResult: The 

600 lone write result from ``write_results``. 

601 

602 Raises: 

603 ValueError: If there are zero write results. This is likely to 

604 **never** occur, since the backend should be stable. 

605 """ 

606 if not write_results: 

607 raise ValueError("Expected at least one write result") 

608 

609 return write_results[0]