Skip to content

Commit

Permalink
Update serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
ruscoder committed Aug 29, 2024
1 parent 4871f2c commit 6c239a3
Show file tree
Hide file tree
Showing 7 changed files with 65 additions and 16 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion fhirpy/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
6 changes: 3 additions & 3 deletions fhirpy/base/lib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions fhirpy/base/lib_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 34 additions & 8 deletions fhirpy/base/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion run_test.sh
Original file line number Diff line number Diff line change
@@ -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
19 changes: 19 additions & 0 deletions tests/test_lib_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit 6c239a3

Please sign in to comment.