From 1979efdd1d22fab2bc5105b7be1fa9df56725d95 Mon Sep 17 00:00:00 2001 From: Todd Sankey Date: Fri, 14 May 2021 16:31:12 -0700 Subject: [PATCH 1/3] Have Query from_repr support the 'kind' format generated by to_repr --- datastore/gcloud/aio/datastore/query.py | 4 ++++ datastore/tests/unit/query_test.py | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/datastore/gcloud/aio/datastore/query.py b/datastore/gcloud/aio/datastore/query.py index 0d03525f8..309a65306 100644 --- a/datastore/gcloud/aio/datastore/query.py +++ b/datastore/gcloud/aio/datastore/query.py @@ -59,6 +59,10 @@ def __eq__(self, other: Any) -> bool: @classmethod def from_repr(cls, data: Dict[str, Any]) -> 'Query': kind = data['kind'] or '' # Kind is required + if not isinstance(kind, str): + # If 'kind' is not a str, then the only other acceptable format + # is the list of a single dict [{'name' : kind}] (see to_repr) + kind = kind[0].get('name') or '' orders = [PropertyOrder.from_repr(o) for o in data.get('order', [])] start_cursor = data.get('startCursor') or '' end_cursor = data.get('endCursor') or '' diff --git a/datastore/tests/unit/query_test.py b/datastore/tests/unit/query_test.py index 7fde63667..26286c292 100644 --- a/datastore/tests/unit/query_test.py +++ b/datastore/tests/unit/query_test.py @@ -108,6 +108,11 @@ def test_to_repr_query_with_several_orders(): def test_repr_returns_to_repr_as_string(query): assert repr(query) == str(query.to_repr()) + @staticmethod + def test_from_to_repr(query): + new_query = Query.from_repr(query.to_repr()) + assert new_query == query + @staticmethod @pytest.fixture(scope='session') def query(query_filter) -> Query: From aa6723c633956d9bc756bfc6900c5da934b9aa63 Mon Sep 17 00:00:00 2001 From: Todd Sankey Date: Fri, 14 May 2021 16:41:41 -0700 Subject: [PATCH 2/3] Fix indent --- datastore/gcloud/aio/datastore/query.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/datastore/gcloud/aio/datastore/query.py b/datastore/gcloud/aio/datastore/query.py index 309a65306..5b279440d 100644 --- a/datastore/gcloud/aio/datastore/query.py +++ b/datastore/gcloud/aio/datastore/query.py @@ -60,9 +60,9 @@ def __eq__(self, other: Any) -> bool: def from_repr(cls, data: Dict[str, Any]) -> 'Query': kind = data['kind'] or '' # Kind is required if not isinstance(kind, str): - # If 'kind' is not a str, then the only other acceptable format - # is the list of a single dict [{'name' : kind}] (see to_repr) - kind = kind[0].get('name') or '' + # If 'kind' is not a str, then the only other acceptable format + # is the list of a single dict [{'name' : kind}] (see to_repr) + kind = kind[0].get('name') or '' orders = [PropertyOrder.from_repr(o) for o in data.get('order', [])] start_cursor = data.get('startCursor') or '' end_cursor = data.get('endCursor') or '' From c5e8a137e41e4cd30963b0e8861f6038700eb2d6 Mon Sep 17 00:00:00 2001 From: Todd Sankey Date: Wed, 17 Sep 2025 16:50:18 -0700 Subject: [PATCH 3/3] Handle nullable field values --- bigquery/gcloud/aio/bigquery/utils.py | 36 ++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/bigquery/gcloud/aio/bigquery/utils.py b/bigquery/gcloud/aio/bigquery/utils.py index 04a33c91a..683b7a2c7 100644 --- a/bigquery/gcloud/aio/bigquery/utils.py +++ b/bigquery/gcloud/aio/bigquery/utils.py @@ -74,22 +74,22 @@ def parse(field: Dict[str, Any], value: Any) -> Any: represent in a backwards-enough compatible fashion. """ try: - convert: Callable[[Any], Any] = { # type: ignore[assignment] - 'BIGNUMERIC': lambda x: decimal.Decimal( + convert: Callable[[Any, bool], Any] = { # type: ignore[assignment] + 'BIGNUMERIC': lambda x, nullable: decimal.Decimal( x, decimal.Context(prec=77), - ), - 'BOOLEAN': lambda x: x == 'true', - 'BYTES': bytes, - 'FLOAT': float, - 'INTEGER': int, - 'NUMERIC': lambda x: decimal.Decimal( + ) if x is not None or not nullable else None, + 'BOOLEAN': lambda x, nullable: x == 'true' if x is not None or not nullable else None, + 'BYTES': lambda x, nullable: bytes(x) if x is not None or not nullable else None, + 'FLOAT': lambda x, nullable: float(x) if x is not None or not nullable else None, + 'INTEGER': lambda x, nullable: int(x) if x is not None or not nullable else None, + 'NUMERIC': lambda x, nullable: decimal.Decimal( x, decimal.Context(prec=38), - ), - 'RECORD': dict, - 'STRING': str, - 'TIMESTAMP': lambda x: datetime.datetime.fromtimestamp( + ) if x is not None or not nullable else None, + 'RECORD': lambda x, nullable: dict(x) if x is not None or not nullable else None, + 'STRING': lambda x, nullable: str(x) if x is not None or not nullable else None, + 'TIMESTAMP': lambda x, nullable: datetime.datetime.fromtimestamp( float(x), tz=utc, - ), + ) if x is not None or not nullable else None, }[field['type']] except KeyError: # TODO: determine the proper methods for converting the following: @@ -104,7 +104,8 @@ def parse(field: Dict[str, Any], value: Any) -> Any: ) raise - if field['mode'] == 'NULLABLE' and value is None: + nullable = field['mode'] == 'NULLABLE' + if nullable and value is None: return value if field['mode'] == 'REPEATED': @@ -115,15 +116,16 @@ def parse(field: Dict[str, Any], value: Any) -> Any: } for xs in flatten(value)] - return [convert(x) for x in flatten(value)] + return ([convert(x, False) for x in flatten(value)] + if value is not None or not nullable else None) if field['type'] == 'RECORD': return { f['name']: parse(f, x) for f, x in zip(field['fields'], flatten(value)) - } + } if value is not None or not nullable else None - return convert(flatten(value)) + return convert(flatten(value), nullable) def query_response_to_dict(response: Dict[str, Any]) -> List[Dict[str, Any]]: