Skip to content

Commit 6c239a3

Browse files
committed
Update serializer
1 parent 4871f2c commit 6c239a3

File tree

7 files changed

+65
-16
lines changed

7 files changed

+65
-16
lines changed

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.0.9
2+
3+
* Update serializer with removing empty dicts/lists and transforming empty dicts into nulls in lists
4+
15
## 2.0.8
26

37
* Add experimental pluggable client-level dump function

fhirpy/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from .lib import AsyncFHIRClient, SyncFHIRClient
22

33
__title__ = "fhir-py"
4-
__version__ = "2.0.8"
4+
__version__ = "2.0.9"
55
__author__ = "beda.software"
66
__license__ = "None"
77
__copyright__ = "Copyright 2024 beda.software"

fhirpy/base/lib_async.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ async def save(
111111
# _as_dict is a private api used internally
112112
_as_dict: bool = False,
113113
) -> Union[TResource, Any]:
114-
data = serialize(self.dump(resource), drop_dict_null_values=fields is None)
114+
data = serialize(self.dump(resource), remove_nulls=fields is None)
115115
if fields:
116116
if not resource.id:
117117
raise TypeError("Resource `id` is required for update operation")
@@ -171,7 +171,7 @@ async def patch(
171171
response_data = await self._do_request(
172172
"patch",
173173
f"{resource_type}/{resource_id}",
174-
data=serialize(self.dump(kwargs), drop_dict_null_values=False),
174+
data=serialize(self.dump(kwargs), remove_nulls=False),
175175
)
176176

177177
if custom_resource_class:
@@ -473,7 +473,7 @@ async def patch(self, _resource: Any = None, **kwargs) -> TResource:
473473
)
474474
data = serialize(
475475
self.client.dump(_resource if _resource is not None else kwargs),
476-
drop_dict_null_values=False,
476+
remove_nulls=False,
477477
)
478478
response_data = await self.client._do_request(
479479
"PATCH", self.resource_type, data, self.params

fhirpy/base/lib_sync.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def save(
111111
# _as_dict is a private api used internally
112112
_as_dict: bool = False,
113113
) -> Union[TResource, Any]:
114-
data = serialize(self.dump(resource), drop_dict_null_values=fields is None)
114+
data = serialize(self.dump(resource), remove_nulls=fields is None)
115115
if fields:
116116
if not resource.id:
117117
raise TypeError("Resource `id` is required for update operation")
@@ -167,7 +167,7 @@ def patch(
167167
response_data = self._do_request(
168168
"patch",
169169
f"{resource_type}/{resource_id}",
170-
data=serialize(self.dump(kwargs), drop_dict_null_values=False),
170+
data=serialize(self.dump(kwargs), remove_nulls=False),
171171
)
172172

173173
if custom_resource_class:
@@ -473,7 +473,7 @@ def patch(self, _resource: Any = None, **kwargs) -> TResource:
473473

474474
data = serialize(
475475
self.client.dump(_resource if _resource is not None else kwargs),
476-
drop_dict_null_values=False,
476+
remove_nulls=False,
477477
)
478478
response_data = self.client._do_request("patch", self.resource_type, data, self.params)
479479
return self._dict_to_resource(response_data)

fhirpy/base/resource.py

+34-8
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,12 @@ def is_local(self):
251251
pass
252252

253253

254-
def serialize(resource: Any, drop_dict_null_values=True) -> dict:
255-
# TODO: make serialization pluggable
256-
# TODO: add empty dict/array cleanup
254+
def serialize(resource: Any, remove_nulls=True) -> dict:
255+
"""
256+
* empty dicts/lists are always removed
257+
* nulls are removed only for dicts if `remove_nulls` is set
258+
* in lists empty dicts are transformed into nulls because nulls are used for alignment
259+
"""
257260

258261
def convert_fn(item):
259262
if isinstance(item, BaseResource):
@@ -264,17 +267,40 @@ def convert_fn(item):
264267

265268
if _is_serializable_dict_like(item):
266269
# Handle dict-serializable structures like pydantic Model
267-
if drop_dict_null_values:
268-
return _remove_dict_null_values(dict(item)), False
269-
return dict(item), False
270+
item = _remove_dict_empty_values(dict(item))
271+
272+
if remove_nulls:
273+
return _remove_nulls(item), False
274+
return item, False
275+
276+
if isinstance(item, list):
277+
return _transform_list_empty_values_to_null(item), False
270278

271279
return item, False
272280

273281
return convert_values(dict(resource), convert_fn)
274282

275283

276-
def _remove_dict_null_values(d: dict):
277-
return {key: value for key, value in d.items() if value is not None}
284+
def _remove_dict_empty_values(d: dict):
285+
return {key: value for key, value in d.items() if not _is_empty(value)}
286+
287+
288+
def _transform_list_empty_values_to_null(d: list):
289+
return [None if _is_empty(value) else value for value in d]
290+
291+
292+
def _remove_nulls(d: dict):
293+
return {key: value for key, value in d.items() if not _is_null(value)}
294+
295+
296+
def _is_empty(d: Any):
297+
if isinstance(d, (dict, list)):
298+
return not d
299+
return False
300+
301+
302+
def _is_null(d: Any):
303+
return d is None
278304

279305

280306
def _is_serializable_dict_like(item):

run_test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/bin/bash
22

3-
export TEST_COMMAND="pipenv run pytest ${@:-tests/}"
3+
export TEST_COMMAND="pipenv run pytest ${@:-tests/} -vv"
44
docker compose -f docker-compose.tests.yaml up --quiet-pull --exit-code-from app app

tests/test_lib_base.py

+19
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,25 @@ def test_serialize_with_dict_null_values(self, client: Union[SyncFHIRClient, Asy
3232
"id": "patient",
3333
}
3434

35+
def test_serialize_with_empty_array(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
36+
patient = client.resource("Patient", id="patient", generalPractitioner=[])
37+
assert patient.serialize() == {
38+
"resourceType": "Patient",
39+
"id": "patient",
40+
}
41+
42+
def test_serialize_with_empty_dict(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
43+
patient = client.resource(
44+
"Patient",
45+
id="patient",
46+
name=[{"given": ["Name"], "_given": [{}], "text": "Name", "_text": {}}],
47+
)
48+
assert patient.serialize() == {
49+
"resourceType": "Patient",
50+
"id": "patient",
51+
"name": [{"given": ["Name"], "_given": [None], "text": "Name"}],
52+
}
53+
3554
def test_serialize(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
3655
practitioner1 = client.resource("Practitioner", id="pr1")
3756
practitioner2 = client.resource("Practitioner", id="pr2")

0 commit comments

Comments
 (0)