Skip to content

Commit

Permalink
Remove null dict values for post/put
Browse files Browse the repository at this point in the history
  • Loading branch information
ruscoder committed Aug 27, 2024
1 parent 4d4bc42 commit 0a9ca27
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 28 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 2.0.6

* Fix type inference for client.resource
* Remove null values from dict for save/create/update
* Preserve null values in dict for patch

## 2.0.5
* Fix support for 3.9+ by adding missing typing-extensios as dependency #129

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.5"
__version__ = "2.0.6"
__author__ = "beda.software"
__license__ = "None"
__copyright__ = "Copyright 2024 beda.software"
Expand Down
18 changes: 14 additions & 4 deletions fhirpy/base/lib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ async def patch(
raise TypeError("Resource `id` is required for patch operation")

response_data = await self._do_request(
"patch", f"{resource_type}/{resource_id}", data=serialize(kwargs)
"patch",
f"{resource_type}/{resource_id}",
data=serialize(kwargs, drop_dict_null_values=False),
)

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

async def patch(self, **kwargs) -> TResource:
if not self.id:
raise TypeError("Resource `id` is required for delete operation")
super(BaseResource, self).update(**kwargs)
await self.save(fields=list(kwargs.keys()))
response_data = await self.__client__.patch(self.reference, **kwargs)

resource_type = self.resource_type
super(BaseResource, self).clear()
super(BaseResource, self).update(**self.__client__.resource(resource_type, **response_data))

return cast(TResource, self)

async def delete(self):
if not self.id:
raise TypeError("Resource `id` is required for delete operation")
return await self.__client__.delete(self)
return await self.__client__.delete(self.reference)

async def refresh(self) -> TResource:
data = await self.__client__._do_request("get", self._get_path())
Expand Down Expand Up @@ -465,7 +473,9 @@ async def patch(self, _resource: Any = None, **kwargs) -> TResource:
DeprecationWarning,
stacklevel=2,
)
data = serialize(_resource if _resource is not None else kwargs)
data = serialize(
_resource if _resource is not None else kwargs, drop_dict_null_values=False
)
response_data = await self.client._do_request(
"PATCH", self.resource_type, data, self.params
)
Expand Down
18 changes: 14 additions & 4 deletions fhirpy/base/lib_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,9 @@ def patch(
raise TypeError("Resource `id` is required for patch operation")

response_data = self._do_request(
"patch", f"{resource_type}/{resource_id}", data=serialize(kwargs)
"patch",
f"{resource_type}/{resource_id}",
data=serialize(kwargs, drop_dict_null_values=False),
)

if custom_resource_class:
Expand Down Expand Up @@ -278,15 +280,21 @@ def update(self) -> TResource: # type: ignore
return cast(TResource, self)

def patch(self, **kwargs) -> TResource:
if not self.id:
raise TypeError("Resource `id` is required for delete operation")
super(BaseResource, self).update(**kwargs)
self.save(fields=list(kwargs.keys()))
response_data = self.__client__.patch(self.reference, **kwargs)

resource_type = self.resource_type
super(BaseResource, self).clear()
super(BaseResource, self).update(**self.__client__.resource(resource_type, **response_data))

return cast(TResource, self)

def delete(self):
if not self.id:
raise TypeError("Resource `id` is required for delete operation")
return self.__client__.delete(self)
return self.__client__.delete(self.reference)

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

data = serialize(_resource if _resource is not None else kwargs)
data = serialize(
_resource if _resource is not None else kwargs, drop_dict_null_values=False
)
response_data = self.client._do_request("patch", self.resource_type, data, self.params)
return self._dict_to_resource(response_data)

Expand Down
8 changes: 7 additions & 1 deletion fhirpy/base/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def is_local(self):
pass


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

def convert_fn(item):
Expand All @@ -263,13 +263,19 @@ 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

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 _is_serializable_dict_like(item):
"""
>>> _is_serializable_dict_like({})
Expand Down
8 changes: 4 additions & 4 deletions fhirpy/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ def reference(self, resource_type=None, id=None, reference=None, **kwargs): # n
return SyncFHIRReference(self, reference=reference, **kwargs)

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

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

def resource(
Expand Down Expand Up @@ -152,11 +152,11 @@ def reference(
return AsyncFHIRReference(self, reference=reference, **kwargs)

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

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

def resource(
Expand Down
35 changes: 28 additions & 7 deletions tests/test_lib_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from tests.utils import MockAiohttpResponse

from .config import FHIR_SERVER_AUTHORIZATION, FHIR_SERVER_URL
from .types import HumanName, Identifier, Patient
from .types import HumanName, Identifier, Patient, Reference


class TestLibAsyncCase:
Expand Down Expand Up @@ -40,7 +40,7 @@ async def create_resource(self, resource_type, **kwargs):
resource_type, identifier=self.identifier, **kwargs
).create()

async def create_patient_model(self):
async def create_patient_model(self, **kwargs):
patient = Patient(
name=[HumanName(text="My patient")],
identifier=[
Expand All @@ -49,6 +49,7 @@ async def create_patient_model(self):
value=self.identifier[0]["system"],
)
],
**kwargs,
)
return await self.client.create(patient)

Expand Down Expand Up @@ -190,15 +191,22 @@ async def test_client_get_specifying_resource_type_fails_without_id(self):

@pytest.mark.asyncio()
async def test_client_patch_specifying_reference(self):
patient = await self.create_patient_model()
patient = await self.create_patient_model(
managingOrganization=Reference(reference="urn:organization")
)
new_identifier = [*patient.identifier, Identifier(system="url", value="value")]

patched_patient = await self.client.patch(
f"{patient.resourceType}/{patient.id}", identifier=new_identifier
f"{patient.resourceType}/{patient.id}",
identifier=new_identifier,
managingOrganization=None,
)

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

@pytest.mark.asyncio()
async def test_client_patch_specifying_resource_type_str_and_id(self):
Expand Down Expand Up @@ -455,24 +463,35 @@ async def test_patch_with_params__no_match(self):

@pytest.mark.asyncio()
async def test_patch_with_params__one_match(self):
patient = await self.create_resource("Patient", id="patient", active=True)
patient = await self.create_resource(
"Patient",
id="patient",
active=True,
managingOrganization={"reference": "urn:organization"},
)

patched_patient = await (
self.client.resources("Patient")
.search(identifier="fhirpy")
.patch(identifier=self.identifier, name=[{"text": "Indiana Jones"}])
.patch(
identifier=self.identifier,
name=[{"text": "Indiana Jones"}],
managingOrganization=None,
)
)
assert patched_patient.id == patient.id
assert patched_patient.get_by_path(["meta", "versionId"]) != patient.get_by_path(
["meta", "versionId"]
)
assert patched_patient.get_by_path(["name", 0, "text"]) == "Indiana Jones"
assert patched_patient.get("managingOrganization") is None

await patient.refresh()
assert patched_patient.get_by_path(["meta", "versionId"]) == patient.get_by_path(
["meta", "versionId"]
)
assert patient.active is True
assert patient.get("managingOrganization") is None

@pytest.mark.asyncio()
async def test_patch_with_params__one_match_deprecated(self):
Expand Down Expand Up @@ -880,6 +899,7 @@ async def test_patch(self):
name=[{"text": "J London"}],
active=False,
birthDate="1998-01-01",
managingOrganization={"reference": "urn:organization"},
)
new_name = [
{
Expand All @@ -889,13 +909,14 @@ async def test_patch(self):
}
]
patient_instance_2 = self.client.resource("Patient", id=patient_id, birthDate="2001-01-01")
await patient_instance_2.patch(active=True, name=new_name)
await patient_instance_2.patch(active=True, name=new_name, managingOrganization=None)
patient_instance_1_refreshed = await patient_instance_1.to_reference().to_resource()

assert patient_instance_1_refreshed.serialize() == patient_instance_2.serialize()
assert patient_instance_1_refreshed.active is True
assert patient_instance_1_refreshed.birthDate == "1998-01-01"
assert patient_instance_1_refreshed["name"] == new_name
assert patient_instance_1_refreshed.get("managingOrganization") is None

@pytest.mark.asyncio()
async def test_update_without_id(self):
Expand Down
30 changes: 30 additions & 0 deletions tests/test_lib_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import pytest

from fhirpy import AsyncFHIRClient, SyncFHIRClient
from fhirpy.base.resource import serialize
from fhirpy.base.utils import AttrDict, SearchList, parse_pagination_url, set_by_path
from fhirpy.lib import BaseFHIRReference

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

def test_serialize_with_dict_null_values(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
patient = client.resource(
"Patient",
id="patient",
managingOrganization=None,
)
assert patient.serialize() == {
"resourceType": "Patient",
"id": "patient",
}

def test_serialize(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
practitioner1 = client.resource("Practitioner", id="pr1")
practitioner2 = client.resource("Practitioner", id="pr2")
Expand Down Expand Up @@ -241,6 +253,24 @@ def test_pluggable_type_model_resource_instantiation(
assert isinstance(patient.name[0], HumanName)
assert patient.name[0].text == "Name"

def test_pluggable_type_model_serialize_with_dict_null_values(
self, client: Union[SyncFHIRClient, AsyncFHIRClient]
):
patient = client.resource(
Patient,
**{
"resourceType": "Patient",
"identifier": [{"system": "url", "value": "value"}],
"name": [{"text": "Name"}],
"managingOrganization": None,
},
)
assert serialize(patient) == {
"resourceType": "Patient",
"identifier": [{"system": "url", "value": "value"}],
"name": [{"text": "Name"}],
}

def test_resource_resource_type_setter(self, client: Union[SyncFHIRClient, AsyncFHIRClient]):
patient = client.resource("Patient", id="p1")
patient.resourceType = "Patient"
Expand Down
Loading

0 comments on commit 0a9ca27

Please sign in to comment.