|
16 | 16 |
|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
| 19 | +import collections.abc |
19 | 20 | import datetime |
20 | 21 | import json |
21 | 22 | from typing import ( |
|
47 | 48 | from google.cloud.firestore_v1.bson import ( |
48 | 49 | BSONBinary, |
49 | 50 | BSONDecimal128, |
| 51 | + BSONInt32, |
| 52 | + BSONMaxKey, |
| 53 | + BSONMinKey, |
50 | 54 | BSONObjectID, |
51 | 55 | BSONRegex, |
| 56 | + BSONTimestamp, |
52 | 57 | ) |
53 | 58 | from google.cloud.firestore_v1.field_path import FieldPath, parse_field_path |
54 | 59 | from google.cloud.firestore_v1.types import common, document, write |
@@ -419,29 +424,130 @@ def decode_value( |
419 | 424 | raise ValueError("Unknown ``value_type``", value_type) |
420 | 425 |
|
421 | 426 |
|
422 | | -def decode_dict(value_fields, client) -> Union[dict, Vector]: |
| 427 | +def _parse_oid(val: Any) -> BSONObjectID: |
| 428 | + if not isinstance(val, str): |
| 429 | + raise ValueError(f"Invalid BSONObjectID map value, expected str: {val!r}") |
| 430 | + return BSONObjectID(val) |
| 431 | + |
| 432 | + |
| 433 | +def _parse_decimal128(val: Any) -> BSONDecimal128: |
| 434 | + if not isinstance(val, str): |
| 435 | + raise ValueError(f"Invalid BSONDecimal128 map value, expected str: {val!r}") |
| 436 | + return BSONDecimal128(val) |
| 437 | + |
| 438 | + |
| 439 | +def _parse_int32(val: Any) -> BSONInt32: |
| 440 | + if type(val) is not int or isinstance(val, bool): |
| 441 | + raise ValueError(f"Invalid BSONInt32 map value, expected int: {val!r}") |
| 442 | + return BSONInt32(val) |
| 443 | + |
| 444 | + |
| 445 | +def _parse_minkey(val: Any) -> BSONMinKey: |
| 446 | + if type(val) is not int or isinstance(val, bool): |
| 447 | + raise ValueError(f"Invalid BSONMinKey map value, expected int: {val!r}") |
| 448 | + return BSONMinKey() |
| 449 | + |
| 450 | + |
| 451 | +def _parse_maxkey(val: Any) -> BSONMaxKey: |
| 452 | + if type(val) is not int or isinstance(val, bool): |
| 453 | + raise ValueError(f"Invalid BSONMaxKey map value, expected int: {val!r}") |
| 454 | + return BSONMaxKey() |
| 455 | + |
| 456 | + |
| 457 | +def _parse_timestamp(val: Any) -> BSONTimestamp: |
| 458 | + if not isinstance(val, collections.abc.Mapping): |
| 459 | + raise ValueError(f"Invalid BSONTimestamp map value, expected mapping: {val!r}") |
| 460 | + sec = val.get("seconds") |
| 461 | + inc = val.get("increment") |
| 462 | + if ( |
| 463 | + type(sec) is not int |
| 464 | + or type(inc) is not int |
| 465 | + or isinstance(sec, bool) |
| 466 | + or isinstance(inc, bool) |
| 467 | + or len(val) != 2 |
| 468 | + ): |
| 469 | + raise ValueError(f"Invalid BSONTimestamp fields: {val!r}") |
| 470 | + return BSONTimestamp(sec, inc) |
| 471 | + |
| 472 | + |
| 473 | +def _parse_regex(val: Any) -> BSONRegex: |
| 474 | + if not isinstance(val, collections.abc.Mapping): |
| 475 | + raise ValueError(f"Invalid BSONRegex map value, expected mapping: {val!r}") |
| 476 | + pat = val.get("pattern") |
| 477 | + opt = val.get("options", "") |
| 478 | + if not isinstance(pat, str) or not isinstance(opt, str) or len(val) not in (1, 2): |
| 479 | + raise ValueError(f"Invalid BSONRegex fields: {val!r}") |
| 480 | + return BSONRegex(pat, opt) |
| 481 | + |
| 482 | + |
| 483 | +def _parse_binary(val: Any) -> BSONBinary: |
| 484 | + if not isinstance(val, collections.abc.Mapping): |
| 485 | + raise ValueError(f"Invalid BSONBinary map value, expected mapping: {val!r}") |
| 486 | + sub = val.get("sub_type") |
| 487 | + bdata = val.get("bytes") |
| 488 | + if ( |
| 489 | + type(sub) is not int |
| 490 | + or isinstance(sub, bool) |
| 491 | + or not isinstance(bdata, (bytes, bytearray, memoryview)) |
| 492 | + or len(val) != 2 |
| 493 | + ): |
| 494 | + raise ValueError(f"Invalid BSONBinary fields: {val!r}") |
| 495 | + return BSONBinary(bdata, subtype=sub) |
| 496 | + |
| 497 | + |
| 498 | +_BSON_MAP_PARSERS = { |
| 499 | + "__oid__": _parse_oid, |
| 500 | + "__decimal128__": _parse_decimal128, |
| 501 | + "__int__": _parse_int32, |
| 502 | + "__minkey__": _parse_minkey, |
| 503 | + "__maxkey__": _parse_maxkey, |
| 504 | + "__timestamp__": _parse_timestamp, |
| 505 | + "__regex__": _parse_regex, |
| 506 | + "__binary__": _parse_binary, |
| 507 | +} |
| 508 | + |
| 509 | + |
| 510 | +def _parse_bson_mapping(key: str, val: Any) -> Optional[Any]: |
| 511 | + """Converts legacy BSON map value representations to native BSON instances.""" |
| 512 | + parser = _BSON_MAP_PARSERS.get(key) |
| 513 | + if parser is not None: |
| 514 | + return parser(val) |
| 515 | + return None |
| 516 | + |
| 517 | + |
| 518 | +def decode_dict( |
| 519 | + value_fields, client, decode_bson: Optional[bool] = None |
| 520 | +) -> Union[dict, Vector]: |
423 | 521 | """Converts a protobuf map of Firestore ``Value``-s. |
424 | 522 |
|
425 | 523 | Args: |
426 | 524 | value_fields (google.protobuf.pyext._message.MessageMapContainer): A |
427 | 525 | protobuf map of Firestore ``Value``-s. |
428 | 526 | client (:class:`~google.cloud.firestore_v1.client.Client`): |
429 | 527 | A client that has a document factory. |
| 528 | + decode_bson (Optional[bool]): Flag indicating whether to decode BSON map representations. |
430 | 529 |
|
431 | 530 | Returns: |
432 | | - Dict[str, Union[NoneType, bool, int, float, datetime.datetime, \ |
433 | | - str, bytes, dict, ~google.cloud.Firestore.GeoPoint]]: A dictionary |
434 | | - of native Python values converted from the ``value_fields``. |
| 531 | + Dict[str, Any]: A dictionary converted from ``value_fields``. |
435 | 532 | """ |
| 533 | + effective_decode = ( |
| 534 | + decode_bson |
| 535 | + if decode_bson is not None |
| 536 | + else getattr(client, "decode_bson", False) |
| 537 | + ) |
436 | 538 | value_fields_pb = getattr(value_fields, "_pb", value_fields) |
437 | 539 | res = {key: decode_value(value, client) for key, value in value_fields_pb.items()} |
438 | 540 |
|
439 | 541 | if res.get("__type__", None) == "__vector__": |
440 | | - # Vector data type is represented as mapping. |
441 | | - # {"__type__":"__vector__", "value": [1.0, 2.0, 3.0]}. |
442 | 542 | values = cast(Sequence[float], res["value"]) |
443 | 543 | return Vector(values) |
444 | 544 |
|
| 545 | + if effective_decode and len(res) == 1: |
| 546 | + single_key = next(iter(res)) |
| 547 | + parsed = _parse_bson_mapping(single_key, res[single_key]) |
| 548 | + if parsed is not None: |
| 549 | + return parsed |
| 550 | + |
445 | 551 | return res |
446 | 552 |
|
447 | 553 |
|
|
0 commit comments