-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(firestore): add BSON read deserialization support #18402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9d3a07c
36a336c
faa42fd
7eb279e
e2abd58
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -350,7 +350,18 @@ def reference_value_to_document(reference_value, client) -> Any: | |
| def decode_value( | ||
| value, client | ||
| ) -> Union[ | ||
| None, bool, int, float, list, datetime.datetime, str, bytes, dict, GeoPoint, Vector | ||
| None, | ||
| bool, | ||
| int, | ||
| float, | ||
| list, | ||
| datetime.datetime, | ||
| str, | ||
| bytes, | ||
| dict, | ||
| GeoPoint, | ||
| Vector, | ||
| _BSONType, | ||
| ]: | ||
| """Converts a Firestore protobuf ``Value`` to a native Python value. | ||
|
|
||
|
|
@@ -362,7 +373,9 @@ def decode_value( | |
|
|
||
| Returns: | ||
| Union[NoneType, bool, int, float, datetime.datetime, \ | ||
| str, bytes, dict, ~google.cloud.Firestore.GeoPoint]: A native | ||
| str, bytes, dict, ~google.cloud.Firestore.GeoPoint, \ | ||
| ~google.cloud.firestore_v1.vector.Vector, \ | ||
| ~google.cloud.firestore_v1.bson._BSONType]: A native \ | ||
| Python value converted from the ``value``. | ||
|
|
||
| Raises: | ||
|
|
@@ -402,7 +415,22 @@ def decode_value( | |
| raise ValueError("Unknown ``value_type``", value_type) | ||
|
|
||
|
|
||
| def decode_dict(value_fields, client) -> Union[dict, Vector]: | ||
| def _decode_bson_dict_recursive(data: Any) -> Any: | ||
| """Recursively decodes BSON wire map dictionaries.""" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IIUC, This method shouldn't be necessary. decode_dict is already recursive, and should hanle BSON on its own. But let me know if I'm missing something |
||
| if isinstance(data, dict): | ||
| decoded = _BSONType._from_dict(data) | ||
| if decoded is not None: | ||
| return decoded | ||
| return {k: _decode_bson_dict_recursive(v) for k, v in data.items()} | ||
| elif isinstance(data, list): | ||
| return [_decode_bson_dict_recursive(item) for item in data] | ||
| return data | ||
|
|
||
|
|
||
| def decode_dict( | ||
| value_fields, | ||
| client, | ||
| ) -> Union[dict, Vector, _BSONType]: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. bytes too? |
||
| """Converts a protobuf map of Firestore ``Value``-s. | ||
|
|
||
| Args: | ||
|
|
@@ -412,9 +440,9 @@ def decode_dict(value_fields, client) -> Union[dict, Vector]: | |
| A client that has a document factory. | ||
|
|
||
| Returns: | ||
| Dict[str, Union[NoneType, bool, int, float, datetime.datetime, \ | ||
| str, bytes, dict, ~google.cloud.Firestore.GeoPoint]]: A dictionary | ||
| of native Python values converted from the ``value_fields``. | ||
| Union[dict, ~google.cloud.firestore_v1.vector.Vector, \ | ||
| ~google.cloud.firestore_v1.bson._BSONType]: A dictionary of native \ | ||
| Python values, Vector, or BSON object converted from ``value_fields``. | ||
| """ | ||
| value_fields_pb = getattr(value_fields, "_pb", value_fields) | ||
| res = {key: decode_value(value, client) for key, value in value_fields_pb.items()} | ||
|
|
@@ -425,6 +453,10 @@ def decode_dict(value_fields, client) -> Union[dict, Vector]: | |
| values = cast(Sequence[float], res["value"]) | ||
| return Vector(values) | ||
|
|
||
| decoded = _BSONType._from_dict(res) | ||
| if decoded is not None: | ||
| return decoded | ||
|
|
||
| return res | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -525,7 +525,8 @@ def to_dict(self) -> Union[Dict[str, Any], None]: | |
| """ | ||
| if not self._exists: | ||
| return None | ||
| return copy.deepcopy(self._data) | ||
| data = copy.deepcopy(self._data) | ||
| return _helpers._decode_bson_dict_recursive(data) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't need to change, self._data should already be in a good format (i.e., it would have run through _decode_dict before being saved to _data) |
||
|
|
||
| def _to_protobuf(self) -> Optional[Document]: | ||
| return _helpers.document_snapshot_to_protobuf(self) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,7 +27,7 @@ | |
| import abc | ||
| import decimal | ||
| import re | ||
| from typing import Any, Dict, Union | ||
| from typing import Any, Callable, Dict, Union | ||
|
|
||
| __all__ = [ | ||
| "BSONObjectId", | ||
|
|
@@ -65,6 +65,24 @@ def __eq__(self, other: Any) -> bool: | |
| def __hash__(self) -> int: | ||
| """Hash representation contract for set and dictionary keys.""" | ||
|
|
||
| @classmethod | ||
| def _from_dict(cls, data: Any) -> Any: | ||
| """Deserializes a BSON wire map dictionary into a BSON instance or bytes. | ||
|
|
||
| Args: | ||
| data (Any): Potential BSON wire map dictionary. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this really return Any type? I would assume (Try to avoid using Any wherever possible) |
||
|
|
||
| Returns: | ||
| Any: Deserialized BSON container instance/bytes, or None if not a BSON wire map. | ||
| """ | ||
| if not isinstance(data, dict) or len(data) != 1: | ||
| return None | ||
| key, val = next(iter(data.items())) | ||
| decoder = _BSON_DECODERS.get(key) | ||
| if decoder is None: | ||
| return None | ||
| return decoder(val) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we catch exceptions here, so we don't crash when reading data? Maybe fall back to None? |
||
|
|
||
| def __repr__(self) -> str: | ||
| return f"{self.__class__.__name__}()" | ||
|
|
||
|
|
@@ -503,3 +521,21 @@ def __hash__(self) -> int: | |
| return hash(d) | ||
| except decimal.InvalidOperation: | ||
| return hash((type(self), self._value)) | ||
|
|
||
|
|
||
| _BSON_DECODERS: Dict[str, Callable[[Any], Any]] = { | ||
| "__oid__": BSONObjectId, | ||
| "__min__": lambda _: BSONMinKey(), | ||
| "__max__": lambda _: BSONMaxKey(), | ||
| "__int__": BSONInt32, | ||
| "__decimal128__": BSONDecimal128, | ||
| "__binary__": lambda v: (v[1:] if v[0] == 0 else BSONBinary(v[1:], subtype=v[0])) | ||
| if isinstance(v, (bytes, bytearray)) and len(v) >= 1 | ||
| else None, | ||
| "__request_timestamp__": lambda v: BSONTimestamp(v["seconds"], v["increment"]) | ||
| if isinstance(v, dict) and "seconds" in v and "increment" in v | ||
| else None, | ||
| "__regex__": lambda v: BSONRegex(v["pattern"], v.get("options", "")) | ||
| if isinstance(v, dict) and "pattern" in v | ||
| else None, | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: If you wanted to be fancy, there's probably a way we could make these keys part of each class, and build this mapping dynamically. But I think keeping it static works fine too |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IF _BSONType is being exposed in this way, it should actually be public
(Sorry, I think I suggested making it private at first. I didn't see the whole picture at the time, and making things private is my default)