From 6c239a3a71c557a3320375f762d4f7b3509eb057 Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Thu, 29 Aug 2024 22:16:35 +0200 Subject: [PATCH] Update serializer --- CHANGELOG.md | 4 ++++ fhirpy/__init__.py | 2 +- fhirpy/base/lib_async.py | 6 +++--- fhirpy/base/lib_sync.py | 6 +++--- fhirpy/base/resource.py | 42 ++++++++++++++++++++++++++++++++-------- run_test.sh | 2 +- tests/test_lib_base.py | 19 ++++++++++++++++++ 7 files changed, 65 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d815da..ff8437c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.9 + +* Update serializer with removing empty dicts/lists and transforming empty dicts into nulls in lists + ## 2.0.8 * Add experimental pluggable client-level dump function diff --git a/fhirpy/__init__.py b/fhirpy/__init__.py index f36fb8e..8bc50b2 100644 --- a/fhirpy/__init__.py +++ b/fhirpy/__init__.py @@ -1,7 +1,7 @@ from .lib import AsyncFHIRClient, SyncFHIRClient __title__ = "fhir-py" -__version__ = "2.0.8" +__version__ = "2.0.9" __author__ = "beda.software" __license__ = "None" __copyright__ = "Copyright 2024 beda.software" diff --git a/fhirpy/base/lib_async.py b/fhirpy/base/lib_async.py index af02abd..4ef24c6 100644 --- a/fhirpy/base/lib_async.py +++ b/fhirpy/base/lib_async.py @@ -111,7 +111,7 @@ async def save( # _as_dict is a private api used internally _as_dict: bool = False, ) -> Union[TResource, Any]: - data = serialize(self.dump(resource), drop_dict_null_values=fields is None) + data = serialize(self.dump(resource), remove_nulls=fields is None) if fields: if not resource.id: raise TypeError("Resource `id` is required for update operation") @@ -171,7 +171,7 @@ async def patch( response_data = await self._do_request( "patch", f"{resource_type}/{resource_id}", - data=serialize(self.dump(kwargs), drop_dict_null_values=False), + data=serialize(self.dump(kwargs), remove_nulls=False), ) if custom_resource_class: @@ -473,7 +473,7 @@ async def patch(self, _resource: Any = None, **kwargs) -> TResource: ) data = serialize( self.client.dump(_resource if _resource is not None else kwargs), - drop_dict_null_values=False, + remove_nulls=False, ) response_data = await self.client._do_request( "PATCH", self.resource_type, data, self.params diff --git a/fhirpy/base/lib_sync.py b/fhirpy/base/lib_sync.py index 195e148..832a443 100644 --- a/fhirpy/base/lib_sync.py +++ b/fhirpy/base/lib_sync.py @@ -111,7 +111,7 @@ def save( # _as_dict is a private api used internally _as_dict: bool = False, ) -> Union[TResource, Any]: - data = serialize(self.dump(resource), drop_dict_null_values=fields is None) + data = serialize(self.dump(resource), remove_nulls=fields is None) if fields: if not resource.id: raise TypeError("Resource `id` is required for update operation") @@ -167,7 +167,7 @@ def patch( response_data = self._do_request( "patch", f"{resource_type}/{resource_id}", - data=serialize(self.dump(kwargs), drop_dict_null_values=False), + data=serialize(self.dump(kwargs), remove_nulls=False), ) if custom_resource_class: @@ -473,7 +473,7 @@ def patch(self, _resource: Any = None, **kwargs) -> TResource: data = serialize( self.client.dump(_resource if _resource is not None else kwargs), - drop_dict_null_values=False, + remove_nulls=False, ) response_data = self.client._do_request("patch", self.resource_type, data, self.params) return self._dict_to_resource(response_data) diff --git a/fhirpy/base/resource.py b/fhirpy/base/resource.py index 67b9a0a..468a374 100644 --- a/fhirpy/base/resource.py +++ b/fhirpy/base/resource.py @@ -251,9 +251,12 @@ def is_local(self): pass -def serialize(resource: Any, drop_dict_null_values=True) -> dict: - # TODO: make serialization pluggable - # TODO: add empty dict/array cleanup +def serialize(resource: Any, remove_nulls=True) -> dict: + """ + * empty dicts/lists are always removed + * nulls are removed only for dicts if `remove_nulls` is set + * in lists empty dicts are transformed into nulls because nulls are used for alignment + """ def convert_fn(item): if isinstance(item, BaseResource): @@ -264,17 +267,40 @@ def convert_fn(item): if _is_serializable_dict_like(item): # Handle dict-serializable structures like pydantic Model - if drop_dict_null_values: - return _remove_dict_null_values(dict(item)), False - return dict(item), False + item = _remove_dict_empty_values(dict(item)) + + if remove_nulls: + return _remove_nulls(item), False + return item, False + + if isinstance(item, list): + return _transform_list_empty_values_to_null(item), False return item, False return convert_values(dict(resource), convert_fn) -def _remove_dict_null_values(d: dict): - return {key: value for key, value in d.items() if value is not None} +def _remove_dict_empty_values(d: dict): + return {key: value for key, value in d.items() if not _is_empty(value)} + + +def _transform_list_empty_values_to_null(d: list): + return [None if _is_empty(value) else value for value in d] + + +def _remove_nulls(d: dict): + return {key: value for key, value in d.items() if not _is_null(value)} + + +def _is_empty(d: Any): + if isinstance(d, (dict, list)): + return not d + return False + + +def _is_null(d: Any): + return d is None def _is_serializable_dict_like(item): diff --git a/run_test.sh b/run_test.sh index 03b3310..a586317 100755 --- a/run_test.sh +++ b/run_test.sh @@ -1,4 +1,4 @@ #!/bin/bash -export TEST_COMMAND="pipenv run pytest ${@:-tests/}" +export TEST_COMMAND="pipenv run pytest ${@:-tests/} -vv" docker compose -f docker-compose.tests.yaml up --quiet-pull --exit-code-from app app diff --git a/tests/test_lib_base.py b/tests/test_lib_base.py index 271fb63..ddc7a02 100644 --- a/tests/test_lib_base.py +++ b/tests/test_lib_base.py @@ -32,6 +32,25 @@ def test_serialize_with_dict_null_values(self, client: Union[SyncFHIRClient, Asy "id": "patient", } + def test_serialize_with_empty_array(self, client: Union[SyncFHIRClient, AsyncFHIRClient]): + patient = client.resource("Patient", id="patient", generalPractitioner=[]) + assert patient.serialize() == { + "resourceType": "Patient", + "id": "patient", + } + + def test_serialize_with_empty_dict(self, client: Union[SyncFHIRClient, AsyncFHIRClient]): + patient = client.resource( + "Patient", + id="patient", + name=[{"given": ["Name"], "_given": [{}], "text": "Name", "_text": {}}], + ) + assert patient.serialize() == { + "resourceType": "Patient", + "id": "patient", + "name": [{"given": ["Name"], "_given": [None], "text": "Name"}], + } + def test_serialize(self, client: Union[SyncFHIRClient, AsyncFHIRClient]): practitioner1 = client.resource("Practitioner", id="pr1") practitioner2 = client.resource("Practitioner", id="pr2")