Skip to content

Commit 0a9ca27

Browse files
committed
Remove null dict values for post/put
1 parent 4d4bc42 commit 0a9ca27

File tree

10 files changed

+137
-28
lines changed

10 files changed

+137
-28
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## 2.0.6
2+
3+
* Fix type inference for client.resource
4+
* Remove null values from dict for save/create/update
5+
* Preserve null values in dict for patch
6+
17
## 2.0.5
28
* Fix support for 3.9+ by adding missing typing-extensios as dependency #129
39

fhirpy/__init__.py

Lines changed: 1 addition & 1 deletion
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.5"
4+
__version__ = "2.0.6"
55
__author__ = "beda.software"
66
__license__ = "None"
77
__copyright__ = "Copyright 2024 beda.software"

fhirpy/base/lib_async.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,9 @@ async def patch(
167167
raise TypeError("Resource `id` is required for patch operation")
168168

169169
response_data = await self._do_request(
170-
"patch", f"{resource_type}/{resource_id}", data=serialize(kwargs)
170+
"patch",
171+
f"{resource_type}/{resource_id}",
172+
data=serialize(kwargs, drop_dict_null_values=False),
171173
)
172174

173175
if custom_resource_class:
@@ -284,15 +286,21 @@ async def update(self) -> TResource: # type: ignore
284286
return cast(TResource, self)
285287

286288
async def patch(self, **kwargs) -> TResource:
289+
if not self.id:
290+
raise TypeError("Resource `id` is required for delete operation")
287291
super(BaseResource, self).update(**kwargs)
288-
await self.save(fields=list(kwargs.keys()))
292+
response_data = await self.__client__.patch(self.reference, **kwargs)
293+
294+
resource_type = self.resource_type
295+
super(BaseResource, self).clear()
296+
super(BaseResource, self).update(**self.__client__.resource(resource_type, **response_data))
289297

290298
return cast(TResource, self)
291299

292300
async def delete(self):
293301
if not self.id:
294302
raise TypeError("Resource `id` is required for delete operation")
295-
return await self.__client__.delete(self)
303+
return await self.__client__.delete(self.reference)
296304

297305
async def refresh(self) -> TResource:
298306
data = await self.__client__._do_request("get", self._get_path())
@@ -465,7 +473,9 @@ async def patch(self, _resource: Any = None, **kwargs) -> TResource:
465473
DeprecationWarning,
466474
stacklevel=2,
467475
)
468-
data = serialize(_resource if _resource is not None else kwargs)
476+
data = serialize(
477+
_resource if _resource is not None else kwargs, drop_dict_null_values=False
478+
)
469479
response_data = await self.client._do_request(
470480
"PATCH", self.resource_type, data, self.params
471481
)

fhirpy/base/lib_sync.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ def patch(
163163
raise TypeError("Resource `id` is required for patch operation")
164164

165165
response_data = self._do_request(
166-
"patch", f"{resource_type}/{resource_id}", data=serialize(kwargs)
166+
"patch",
167+
f"{resource_type}/{resource_id}",
168+
data=serialize(kwargs, drop_dict_null_values=False),
167169
)
168170

169171
if custom_resource_class:
@@ -278,15 +280,21 @@ def update(self) -> TResource: # type: ignore
278280
return cast(TResource, self)
279281

280282
def patch(self, **kwargs) -> TResource:
283+
if not self.id:
284+
raise TypeError("Resource `id` is required for delete operation")
281285
super(BaseResource, self).update(**kwargs)
282-
self.save(fields=list(kwargs.keys()))
286+
response_data = self.__client__.patch(self.reference, **kwargs)
287+
288+
resource_type = self.resource_type
289+
super(BaseResource, self).clear()
290+
super(BaseResource, self).update(**self.__client__.resource(resource_type, **response_data))
283291

284292
return cast(TResource, self)
285293

286294
def delete(self):
287295
if not self.id:
288296
raise TypeError("Resource `id` is required for delete operation")
289-
return self.__client__.delete(self)
297+
return self.__client__.delete(self.reference)
290298

291299
def refresh(self) -> TResource:
292300
data = self.__client__._do_request("get", self._get_path())
@@ -465,7 +473,9 @@ def patch(self, _resource: Any = None, **kwargs) -> TResource:
465473
stacklevel=2,
466474
)
467475

468-
data = serialize(_resource if _resource is not None else kwargs)
476+
data = serialize(
477+
_resource if _resource is not None else kwargs, drop_dict_null_values=False
478+
)
469479
response_data = self.client._do_request("patch", self.resource_type, data, self.params)
470480
return self._dict_to_resource(response_data)
471481

fhirpy/base/resource.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ def is_local(self):
251251
pass
252252

253253

254-
def serialize(resource: Any) -> dict:
254+
def serialize(resource: Any, drop_dict_null_values=True) -> dict:
255255
# TODO: make serialization pluggable
256256

257257
def convert_fn(item):
@@ -263,13 +263,19 @@ def convert_fn(item):
263263

264264
if _is_serializable_dict_like(item):
265265
# Handle dict-serializable structures like pydantic Model
266+
if drop_dict_null_values:
267+
return _remove_dict_null_values(dict(item)), False
266268
return dict(item), False
267269

268270
return item, False
269271

270272
return convert_values(dict(resource), convert_fn)
271273

272274

275+
def _remove_dict_null_values(d: dict):
276+
return {key: value for key, value in d.items() if value is not None}
277+
278+
273279
def _is_serializable_dict_like(item):
274280
"""
275281
>>> _is_serializable_dict_like({})

fhirpy/lib.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ def reference(self, resource_type=None, id=None, reference=None, **kwargs): # n
107107
return SyncFHIRReference(self, reference=reference, **kwargs)
108108

109109
@overload
110-
def resource(self, resource_type: str, **kwargs) -> SyncFHIRResource:
110+
def resource(self, resource_type: type[TResource], **kwargs) -> TResource:
111111
...
112112

113113
@overload
114-
def resource(self, resource_type: type[TResource], **kwargs) -> TResource:
114+
def resource(self, resource_type: str, **kwargs) -> SyncFHIRResource:
115115
...
116116

117117
def resource(
@@ -152,11 +152,11 @@ def reference(
152152
return AsyncFHIRReference(self, reference=reference, **kwargs)
153153

154154
@overload
155-
def resource(self, resource_type: str, **kwargs) -> AsyncFHIRResource:
155+
def resource(self, resource_type: type[TResource], **kwargs) -> TResource:
156156
...
157157

158158
@overload
159-
def resource(self, resource_type: type[TResource], **kwargs) -> TResource:
159+
def resource(self, resource_type: str, **kwargs) -> AsyncFHIRResource:
160160
...
161161

162162
def resource(

tests/test_lib_async.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from tests.utils import MockAiohttpResponse
1313

1414
from .config import FHIR_SERVER_AUTHORIZATION, FHIR_SERVER_URL
15-
from .types import HumanName, Identifier, Patient
15+
from .types import HumanName, Identifier, Patient, Reference
1616

1717

1818
class TestLibAsyncCase:
@@ -40,7 +40,7 @@ async def create_resource(self, resource_type, **kwargs):
4040
resource_type, identifier=self.identifier, **kwargs
4141
).create()
4242

43-
async def create_patient_model(self):
43+
async def create_patient_model(self, **kwargs):
4444
patient = Patient(
4545
name=[HumanName(text="My patient")],
4646
identifier=[
@@ -49,6 +49,7 @@ async def create_patient_model(self):
4949
value=self.identifier[0]["system"],
5050
)
5151
],
52+
**kwargs,
5253
)
5354
return await self.client.create(patient)
5455

@@ -190,15 +191,22 @@ async def test_client_get_specifying_resource_type_fails_without_id(self):
190191

191192
@pytest.mark.asyncio()
192193
async def test_client_patch_specifying_reference(self):
193-
patient = await self.create_patient_model()
194+
patient = await self.create_patient_model(
195+
managingOrganization=Reference(reference="urn:organization")
196+
)
194197
new_identifier = [*patient.identifier, Identifier(system="url", value="value")]
195198

196199
patched_patient = await self.client.patch(
197-
f"{patient.resourceType}/{patient.id}", identifier=new_identifier
200+
f"{patient.resourceType}/{patient.id}",
201+
identifier=new_identifier,
202+
managingOrganization=None,
198203
)
199204

200205
assert isinstance(patched_patient, dict)
201206
assert len(patched_patient["identifier"]) == 2 # noqa: PLR2004
207+
assert patched_patient["name"] == [{"text": "My patient"}]
208+
assert patched_patient.get("managingOrganization") is None
209+
assert patched_patient["id"] == patient.id
202210

203211
@pytest.mark.asyncio()
204212
async def test_client_patch_specifying_resource_type_str_and_id(self):
@@ -455,24 +463,35 @@ async def test_patch_with_params__no_match(self):
455463

456464
@pytest.mark.asyncio()
457465
async def test_patch_with_params__one_match(self):
458-
patient = await self.create_resource("Patient", id="patient", active=True)
466+
patient = await self.create_resource(
467+
"Patient",
468+
id="patient",
469+
active=True,
470+
managingOrganization={"reference": "urn:organization"},
471+
)
459472

460473
patched_patient = await (
461474
self.client.resources("Patient")
462475
.search(identifier="fhirpy")
463-
.patch(identifier=self.identifier, name=[{"text": "Indiana Jones"}])
476+
.patch(
477+
identifier=self.identifier,
478+
name=[{"text": "Indiana Jones"}],
479+
managingOrganization=None,
480+
)
464481
)
465482
assert patched_patient.id == patient.id
466483
assert patched_patient.get_by_path(["meta", "versionId"]) != patient.get_by_path(
467484
["meta", "versionId"]
468485
)
469486
assert patched_patient.get_by_path(["name", 0, "text"]) == "Indiana Jones"
487+
assert patched_patient.get("managingOrganization") is None
470488

471489
await patient.refresh()
472490
assert patched_patient.get_by_path(["meta", "versionId"]) == patient.get_by_path(
473491
["meta", "versionId"]
474492
)
475493
assert patient.active is True
494+
assert patient.get("managingOrganization") is None
476495

477496
@pytest.mark.asyncio()
478497
async def test_patch_with_params__one_match_deprecated(self):
@@ -880,6 +899,7 @@ async def test_patch(self):
880899
name=[{"text": "J London"}],
881900
active=False,
882901
birthDate="1998-01-01",
902+
managingOrganization={"reference": "urn:organization"},
883903
)
884904
new_name = [
885905
{
@@ -889,13 +909,14 @@ async def test_patch(self):
889909
}
890910
]
891911
patient_instance_2 = self.client.resource("Patient", id=patient_id, birthDate="2001-01-01")
892-
await patient_instance_2.patch(active=True, name=new_name)
912+
await patient_instance_2.patch(active=True, name=new_name, managingOrganization=None)
893913
patient_instance_1_refreshed = await patient_instance_1.to_reference().to_resource()
894914

895915
assert patient_instance_1_refreshed.serialize() == patient_instance_2.serialize()
896916
assert patient_instance_1_refreshed.active is True
897917
assert patient_instance_1_refreshed.birthDate == "1998-01-01"
898918
assert patient_instance_1_refreshed["name"] == new_name
919+
assert patient_instance_1_refreshed.get("managingOrganization") is None
899920

900921
@pytest.mark.asyncio()
901922
async def test_update_without_id(self):

tests/test_lib_base.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from fhirpy import AsyncFHIRClient, SyncFHIRClient
6+
from fhirpy.base.resource import serialize
67
from fhirpy.base.utils import AttrDict, SearchList, parse_pagination_url, set_by_path
78
from fhirpy.lib import BaseFHIRReference
89

@@ -20,6 +21,17 @@ def test_to_reference_for_reference(self, client: Union[SyncFHIRClient, AsyncFHI
2021
"display": "patient",
2122
}
2223

24+
def test_serialize_with_dict_null_values(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
25+
patient = client.resource(
26+
"Patient",
27+
id="patient",
28+
managingOrganization=None,
29+
)
30+
assert patient.serialize() == {
31+
"resourceType": "Patient",
32+
"id": "patient",
33+
}
34+
2335
def test_serialize(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
2436
practitioner1 = client.resource("Practitioner", id="pr1")
2537
practitioner2 = client.resource("Practitioner", id="pr2")
@@ -241,6 +253,24 @@ def test_pluggable_type_model_resource_instantiation(
241253
assert isinstance(patient.name[0], HumanName)
242254
assert patient.name[0].text == "Name"
243255

256+
def test_pluggable_type_model_serialize_with_dict_null_values(
257+
self, client: Union[SyncFHIRClient, AsyncFHIRClient]
258+
):
259+
patient = client.resource(
260+
Patient,
261+
**{
262+
"resourceType": "Patient",
263+
"identifier": [{"system": "url", "value": "value"}],
264+
"name": [{"text": "Name"}],
265+
"managingOrganization": None,
266+
},
267+
)
268+
assert serialize(patient) == {
269+
"resourceType": "Patient",
270+
"identifier": [{"system": "url", "value": "value"}],
271+
"name": [{"text": "Name"}],
272+
}
273+
244274
def test_resource_resource_type_setter(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
245275
patient = client.resource("Patient", id="p1")
246276
patient.resourceType = "Patient"

0 commit comments

Comments
 (0)