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