compressedfhir 1.0.3__py3-none-any.whl → 1.0.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of compressedfhir might be problematic. Click here for more details.

@@ -61,14 +61,10 @@ class FhirResource(CompressedDict[str, Any]):
61
61
  else None
62
62
  )
63
63
 
64
- def json(self) -> str:
65
- """Convert the resource to a JSON string."""
66
- return json.dumps(obj=self.dict(), cls=FhirJSONEncoder)
67
-
68
64
  def __deepcopy__(self, memo: Dict[int, Any]) -> "FhirResource":
69
65
  """Create a copy of the resource."""
70
66
  return FhirResource(
71
- initial_dict=super().dict(),
67
+ initial_dict=super().raw_dict(),
72
68
  storage_mode=self._storage_mode,
73
69
  )
74
70
 
@@ -84,29 +80,6 @@ class FhirResource(CompressedDict[str, Any]):
84
80
  """
85
81
  return copy.deepcopy(self)
86
82
 
87
- @override
88
- def dict(self, *, remove_nulls: bool = True) -> OrderedDict[str, Any]:
89
- """
90
- Converts the FhirResource object to a dictionary.
91
-
92
- :param remove_nulls: If True, removes None values from the dictionary.
93
- :return: A dictionary representation of the FhirResource object.
94
- """
95
- ordered_dict = super().dict()
96
- result: OrderedDict[str, Any] = copy.deepcopy(ordered_dict)
97
- if remove_nulls:
98
- result = FhirClientJsonHelpers.remove_empty_elements_from_ordered_dict(
99
- result
100
- )
101
-
102
- return result
103
-
104
- def remove_nulls(self) -> None:
105
- """
106
- Removes None values from the resource dictionary.
107
- """
108
- self.replace(value=self.dict(remove_nulls=True))
109
-
110
83
  @property
111
84
  def id(self) -> Optional[str]:
112
85
  """Get the ID from the resource dictionary."""
@@ -171,3 +144,15 @@ class FhirResource(CompressedDict[str, Any]):
171
144
  properties_to_cache=properties_to_cache,
172
145
  ),
173
146
  )
147
+
148
+ @override
149
+ def json(self) -> str:
150
+ """Convert the resource to a JSON string."""
151
+
152
+ # working_dict preserves the python types so create a fhir friendly version
153
+ raw_dict: OrderedDict[str, Any] = self.raw_dict()
154
+
155
+ raw_dict = FhirClientJsonHelpers.remove_empty_elements_from_ordered_dict(
156
+ raw_dict
157
+ )
158
+ return json.dumps(obj=raw_dict, cls=FhirJSONEncoder)
@@ -43,7 +43,7 @@ class FhirResourceMap:
43
43
  """
44
44
  result: OrderedDict[str, Any] = OrderedDict[str, Any]()
45
45
  for key, value in self._resource_map.items():
46
- result[key] = [resource.dict(remove_nulls=True) for resource in value]
46
+ result[key] = [resource.dict() for resource in value]
47
47
  return result
48
48
 
49
49
  def get(self, *, resource_type: str) -> Optional[FhirResourceList]:
@@ -102,124 +102,3 @@ class TestFhirResource:
102
102
  assert parsed_json == initial_data
103
103
  assert "resourceType" in parsed_json
104
104
  assert "id" in parsed_json
105
-
106
-
107
- class TestFhirResourceRemoveNulls:
108
- def test_remove_nulls_simple_dict(self) -> None:
109
- """
110
- Test removing None values from a simple dictionary
111
- """
112
- initial_dict: Dict[str, Any] = {
113
- "name": "John Doe",
114
- "age": None,
115
- "active": True,
116
- "email": None,
117
- }
118
- resource = FhirResource(initial_dict=initial_dict)
119
- resource.remove_nulls()
120
-
121
- with resource.transaction():
122
- # Check that None values are removed
123
- assert "age" not in resource
124
- assert "email" not in resource
125
- assert resource.get("name") == "John Doe"
126
- assert resource.get("active") is True
127
-
128
- def test_remove_nulls_nested_dict(self) -> None:
129
- """
130
- Test removing None values from a nested dictionary
131
- """
132
- initial_dict: Dict[str, Any] = {
133
- "patient": {
134
- "name": "Jane Smith",
135
- "contact": None,
136
- "address": {"street": None, "city": "New York"},
137
- },
138
- "status": None,
139
- }
140
- resource = FhirResource(initial_dict=initial_dict)
141
- resource.remove_nulls()
142
-
143
- with resource.transaction():
144
- assert "status" not in resource
145
- assert "contact" not in resource.get("patient", {})
146
- assert resource.get("patient", {}).get("address", {}).get("street") is None
147
- assert (
148
- resource.get("patient", {}).get("address", {}).get("city") == "New York"
149
- )
150
-
151
- def test_remove_nulls_list_of_dicts(self) -> None:
152
- """
153
- Test removing None values from a list of dictionaries
154
- """
155
- initial_dict: Dict[str, Any] = {
156
- "patients": [
157
- {"name": "Alice", "age": None},
158
- {"name": "Bob", "age": 30},
159
- {"name": None, "active": False},
160
- ]
161
- }
162
- resource = FhirResource(initial_dict=initial_dict)
163
- resource.remove_nulls()
164
-
165
- with resource.transaction():
166
- assert len(resource.get("patients", [])) == 3
167
- assert resource.get("patients", [])[0].get("name") == "Alice"
168
- assert resource.get("patients", [])[1].get("name") == "Bob"
169
- assert resource.get("patients", [])[1].get("age") == 30
170
-
171
- def test_remove_nulls_empty_dict(self) -> None:
172
- """
173
- Test removing None values from an empty dictionary
174
- """
175
- resource = FhirResource(initial_dict={})
176
- resource.remove_nulls()
177
-
178
- assert len(resource) == 0
179
-
180
- def test_remove_nulls_no_changes(self) -> None:
181
- """
182
- Test removing None values when no None values exist
183
- """
184
- initial_dict: Dict[str, Any] = {
185
- "name": "Test User",
186
- "active": True,
187
- "score": 100,
188
- }
189
- resource = FhirResource(initial_dict=initial_dict)
190
- original_dict = resource.copy()
191
- resource.remove_nulls()
192
-
193
- assert resource == original_dict
194
-
195
- def test_remove_nulls_with_custom_storage_mode(self) -> None:
196
- """
197
- Test removing None values with a custom storage mode
198
- """
199
- initial_dict: Dict[str, Any] = {
200
- "name": "Custom Mode User",
201
- "email": None,
202
- "active": True,
203
- }
204
- resource = FhirResource(
205
- initial_dict=initial_dict, storage_mode=CompressedDictStorageMode.default()
206
- )
207
- resource.remove_nulls()
208
-
209
- with resource.transaction():
210
- assert "email" not in resource
211
- assert resource.get("name") == "Custom Mode User"
212
- assert resource.get("active") is True
213
-
214
- def test_remove_nulls_preserves_false_and_zero_values(self) -> None:
215
- """
216
- Test that False and 0 values are not removed
217
- """
218
- initial_dict: Dict[str, Any] = {"active": False, "score": 0, "name": None}
219
- resource = FhirResource(initial_dict=initial_dict)
220
- resource.remove_nulls()
221
-
222
- with resource.transaction():
223
- assert resource.get("active") is False
224
- assert resource.get("score") == 0
225
- assert "name" not in resource
@@ -1,4 +1,5 @@
1
1
  import copy
2
+ import json
2
3
  from collections.abc import KeysView, ValuesView, ItemsView, MutableMapping
3
4
  from contextlib import contextmanager
4
5
  from typing import Dict, Optional, Iterator, cast, List, Any, overload, OrderedDict
@@ -13,6 +14,7 @@ from compressedfhir.utilities.compressed_dict.v1.compressed_dict_storage_mode im
13
14
  CompressedDictStorageMode,
14
15
  CompressedDictStorageType,
15
16
  )
17
+ from compressedfhir.utilities.fhir_json_encoder import FhirJSONEncoder
16
18
  from compressedfhir.utilities.json_serializers.type_preservation_serializer import (
17
19
  TypePreservationSerializer,
18
20
  )
@@ -176,9 +178,7 @@ class CompressedDict[K, V](MutableMapping[K, V]):
176
178
  assert isinstance(dictionary, OrderedDict)
177
179
  if storage_type == "compressed":
178
180
  # Serialize to JSON and compress with zlib
179
- json_str = TypePreservationSerializer.serialize(
180
- dictionary, separators=(",", ":")
181
- )
181
+ json_str = TypePreservationSerializer.serialize(dictionary)
182
182
  return zlib.compress(
183
183
  json_str.encode("utf-8"), level=zlib.Z_BEST_COMPRESSION
184
184
  )
@@ -219,9 +219,7 @@ class CompressedDict[K, V](MutableMapping[K, V]):
219
219
  decompressed_bytes: bytes = zlib.decompress(serialized_dict_bytes)
220
220
  decoded_text: str = decompressed_bytes.decode("utf-8")
221
221
  # noinspection PyTypeChecker
222
- decompressed_dict = TypePreservationSerializer.deserialize(
223
- decoded_text, object_pairs_hook=OrderedDict
224
- )
222
+ decompressed_dict = TypePreservationSerializer.deserialize(decoded_text)
225
223
  assert isinstance(decompressed_dict, OrderedDict)
226
224
  return cast(OrderedDict[K, V], decompressed_dict)
227
225
 
@@ -427,19 +425,43 @@ class CompressedDict[K, V](MutableMapping[K, V]):
427
425
  """
428
426
  return self._get_dict().items()
429
427
 
430
- def dict(self, *, remove_nulls: bool = True) -> OrderedDict[K, V]:
428
+ def raw_dict(self) -> OrderedDict[K, V]:
431
429
  """
432
- Convert to a standard dictionary
430
+ Returns the raw dictionary. Deserializes if necessary.
431
+ Note that this dictionary preserves the python types so it is not FHIR friendly.
432
+ Use dict() if you want a FHIR friendly version.
433
433
 
434
434
  Returns:
435
- Standard dictionary with all values
435
+ raw dictionary
436
436
  """
437
437
  if self._working_dict:
438
438
  return self._working_dict
439
439
  else:
440
- # if the working dict is None, return it but don't store it in the self._working_dict to keep memory low
440
+ # if the working dict is not None, return it but don't store it in the self._working_dict to keep memory low
441
441
  return self.create_working_dict()
442
442
 
443
+ def dict(self) -> OrderedDict[K, V]:
444
+ """
445
+ Convert to a FHIR friendly dictionary where the python types like datetime are converted to string versions
446
+
447
+ Returns:
448
+ FHIR friendly dictionary
449
+ """
450
+ return cast(
451
+ OrderedDict[K, V],
452
+ json.loads(
453
+ self.json(),
454
+ object_pairs_hook=lambda pairs: OrderedDict(pairs),
455
+ ),
456
+ )
457
+
458
+ def json(self) -> str:
459
+ """Convert the resource to a JSON string."""
460
+
461
+ raw_dict: OrderedDict[K, V] = self.raw_dict()
462
+
463
+ return json.dumps(obj=raw_dict, cls=FhirJSONEncoder)
464
+
443
465
  def __repr__(self) -> str:
444
466
  """
445
467
  String representation of the dictionary
@@ -563,7 +585,7 @@ class CompressedDict[K, V](MutableMapping[K, V]):
563
585
  """
564
586
  # Create a new instance with the same storage mode
565
587
  new_instance = CompressedDict(
566
- initial_dict=copy.deepcopy(self.dict()),
588
+ initial_dict=copy.deepcopy(self.raw_dict()),
567
589
  storage_mode=self._storage_mode,
568
590
  properties_to_cache=self._properties_to_cache,
569
591
  )
@@ -637,7 +659,7 @@ class CompressedDict[K, V](MutableMapping[K, V]):
637
659
  Returns:
638
660
  Plain dictionary
639
661
  """
640
- return OrderedDictToDictConverter.convert(self.dict())
662
+ return OrderedDictToDictConverter.convert(self.raw_dict())
641
663
 
642
664
  @classmethod
643
665
  def from_json(cls, json_str: str) -> "CompressedDict[K, V]":
@@ -1,3 +1,5 @@
1
+ from datetime import datetime
2
+
1
3
  import pytest
2
4
  from typing import Any, cast
3
5
 
@@ -232,7 +234,7 @@ def test_transaction_basic_raw_storage() -> None:
232
234
 
233
235
  # After transaction
234
236
  assert compressed_dict._transaction_depth == 0
235
- assert compressed_dict.dict() == {
237
+ assert compressed_dict.raw_dict() == {
236
238
  "key1": "value1",
237
239
  "key2": "value2",
238
240
  "key3": "value3",
@@ -260,7 +262,7 @@ def test_transaction_nested_context() -> None:
260
262
  assert compressed_dict._transaction_depth == 1
261
263
 
262
264
  assert compressed_dict._transaction_depth == 0
263
- assert compressed_dict.dict() == {"key1": "value1", "key2": "value2"}
265
+ assert compressed_dict.raw_dict() == {"key1": "value1", "key2": "value2"}
264
266
 
265
267
 
266
268
  def test_transaction_access_error() -> None:
@@ -309,7 +311,7 @@ def test_transaction_different_storage_modes() -> None:
309
311
  with compressed_dict.transaction() as d:
310
312
  d["key2"] = "value2"
311
313
 
312
- assert compressed_dict.dict() == {"key1": "value1", "key2": "value2"}
314
+ assert compressed_dict.raw_dict() == {"key1": "value1", "key2": "value2"}
313
315
 
314
316
 
315
317
  def test_transaction_with_properties_to_cache() -> None:
@@ -328,7 +330,7 @@ def test_transaction_with_properties_to_cache() -> None:
328
330
  with compressed_dict.transaction() as d:
329
331
  d["key2"] = "value2"
330
332
 
331
- assert compressed_dict.dict() == {
333
+ assert compressed_dict.raw_dict() == {
332
334
  "key1": "value1",
333
335
  "important_prop": "cached_value",
334
336
  "key2": "value2",
@@ -358,3 +360,108 @@ def test_transaction_error_handling() -> None:
358
360
  # Verify the dictionary state remains unchanged
359
361
  with compressed_dict.transaction() as d:
360
362
  assert d.dict() == {"key1": "value1", "key2": "value2"}
363
+
364
+
365
+ def test_nested_dict_with_datetime() -> None:
366
+ nested_dict = {
367
+ "beneficiary": {"reference": "Patient/1234567890123456703", "type": "Patient"},
368
+ "class": [
369
+ {
370
+ "name": "Aetna Plan",
371
+ "type": {
372
+ "coding": [
373
+ {
374
+ "code": "plan",
375
+ "display": "Plan",
376
+ "system": "http://terminology.hl7.org/CodeSystem/coverage-class",
377
+ }
378
+ ]
379
+ },
380
+ "value": "AE303",
381
+ }
382
+ ],
383
+ "costToBeneficiary": [
384
+ {
385
+ "type": {"text": "Annual Physical Exams NMC - In Network"},
386
+ "valueQuantity": {
387
+ "system": "http://aetna.com/Medicare/CostToBeneficiary/ValueQuantity/code",
388
+ "unit": "$",
389
+ "value": 50.0,
390
+ },
391
+ }
392
+ ],
393
+ "id": "3456789012345670304",
394
+ "identifier": [
395
+ {
396
+ "system": "https://sources.aetna.com/coverage/identifier/membershipid/59",
397
+ "type": {
398
+ "coding": [
399
+ {
400
+ "code": "SN",
401
+ "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
402
+ }
403
+ ]
404
+ },
405
+ "value": "435679010300+AE303+2021-01-01",
406
+ },
407
+ {
408
+ "id": "uuid",
409
+ "system": "https://www.icanbwell.com/uuid",
410
+ "value": "92266603-aa8b-58c6-99bd-326fd1da1896",
411
+ },
412
+ ],
413
+ "meta": {
414
+ "security": [
415
+ {"code": "aetna", "system": "https://www.icanbwell.com/owner"},
416
+ {"code": "aetna", "system": "https://www.icanbwell.com/access"},
417
+ {"code": "aetna", "system": "https://www.icanbwell.com/vendor"},
418
+ {"code": "proa", "system": "https://www.icanbwell.com/connectionType"},
419
+ ],
420
+ "source": "http://mock-server:1080/test_patient_access_transformer/source/4_0_0/Coverage/3456789012345670304",
421
+ },
422
+ "network": "Medicare - MA/NY/NJ - Full Reciprocity",
423
+ "payor": [
424
+ {
425
+ "display": "Aetna",
426
+ "reference": "Organization/6667778889990000015",
427
+ "type": "Organization",
428
+ }
429
+ ],
430
+ "period": {
431
+ "end": datetime.fromisoformat("2021-12-31").date(),
432
+ "start": datetime.fromisoformat("2021-01-01").date(),
433
+ },
434
+ "policyHolder": {"reference": "Patient/1234567890123456703", "type": "Patient"},
435
+ "relationship": {
436
+ "coding": [
437
+ {
438
+ "code": "self",
439
+ "system": "http://terminology.hl7.org/CodeSystem/subscriber-relationship",
440
+ }
441
+ ]
442
+ },
443
+ "resourceType": "Coverage",
444
+ "status": "active",
445
+ "subscriber": {"reference": "Patient/1234567890123456703", "type": "Patient"},
446
+ "subscriberId": "435679010300",
447
+ "type": {
448
+ "coding": [
449
+ {
450
+ "code": "PPO",
451
+ "display": "preferred provider organization policy",
452
+ "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
453
+ }
454
+ ]
455
+ },
456
+ }
457
+
458
+ compressed_dict = CompressedDict(
459
+ initial_dict=nested_dict,
460
+ storage_mode=CompressedDictStorageMode.compressed(),
461
+ properties_to_cache=[],
462
+ )
463
+
464
+ plain_dict = compressed_dict.to_plain_dict()
465
+
466
+ assert plain_dict["period"]["start"] == nested_dict["period"]["start"] # type: ignore[index]
467
+ assert plain_dict == nested_dict
@@ -1,7 +1,6 @@
1
1
  from datetime import datetime, date
2
2
  from decimal import Decimal
3
- from typing import Type, Any, Dict
4
-
3
+ from typing import Type, Any, Dict, Optional
5
4
  import pytest
6
5
 
7
6
  from compressedfhir.utilities.json_serializers.type_preservation_decoder import (
@@ -10,14 +9,27 @@ from compressedfhir.utilities.json_serializers.type_preservation_decoder import
10
9
 
11
10
 
12
11
  class TestCustomObject:
13
- def __init__(self, name: str, value: int):
12
+ def __init__(
13
+ self,
14
+ name: str,
15
+ value: int,
16
+ created_at: Optional[datetime] = None,
17
+ nested_data: Optional[Dict[str, Any]] = None,
18
+ ):
14
19
  self.name: str = name
15
20
  self.value: int = value
21
+ self.created_at: Optional[datetime] = created_at
22
+ self.nested_data: Optional[Dict[str, Any]] = nested_data
16
23
 
17
24
  def __eq__(self, other: Any) -> bool:
18
25
  if not isinstance(other, TestCustomObject):
19
26
  return False
20
- return self.name == other.name and self.value == other.value
27
+ return (
28
+ self.name == other.name
29
+ and self.value == other.value
30
+ and self.created_at == other.created_at
31
+ and self.nested_data == other.nested_data
32
+ )
21
33
 
22
34
 
23
35
  @pytest.mark.parametrize(
@@ -42,7 +54,6 @@ def test_complex_type_decoding(
42
54
  Test decoding of various complex types
43
55
  """
44
56
  decoded = TypePreservationDecoder.decode(input_dict)
45
-
46
57
  assert isinstance(decoded, expected_type)
47
58
 
48
59
 
@@ -55,9 +66,7 @@ def test_custom_object_decoding() -> None:
55
66
  "__module__": __name__,
56
67
  "attributes": {"name": "test", "value": 42},
57
68
  }
58
-
59
69
  decoded = TypePreservationDecoder.decode(custom_obj_dict)
60
-
61
70
  assert isinstance(decoded, TestCustomObject)
62
71
  assert decoded.name == "test"
63
72
  assert decoded.value == 42
@@ -74,9 +83,68 @@ def test_custom_decoder() -> None:
74
83
  return data
75
84
 
76
85
  special_dict = {"__type__": "special_type", "value": "test"}
77
-
78
86
  decoded = TypePreservationDecoder.decode(
79
87
  special_dict, custom_decoders={"special_type": custom_decoder}
80
88
  )
81
-
82
89
  assert decoded == "Decoded: test"
90
+
91
+
92
+ def test_nested_datetime_decoding() -> None:
93
+ """
94
+ Test decoding of nested datetime fields
95
+ """
96
+ nested_datetime_dict = {
97
+ "__type__": "TestCustomObject",
98
+ "__module__": __name__,
99
+ "attributes": {
100
+ "name": "test",
101
+ "value": 42,
102
+ "created_at": {"__type__": "datetime", "iso": "2023-06-15T10:30:00"},
103
+ "nested_data": {
104
+ "timestamp": {"__type__": "datetime", "iso": "2023-06-16T15:45:00"}
105
+ },
106
+ },
107
+ }
108
+
109
+ decoded: TestCustomObject = TypePreservationDecoder.decode(nested_datetime_dict)
110
+
111
+ assert isinstance(decoded, TestCustomObject)
112
+ assert decoded.name == "test"
113
+ assert decoded.value == 42
114
+
115
+ # Check nested datetime fields
116
+ assert hasattr(decoded, "created_at")
117
+ assert isinstance(decoded.created_at, datetime)
118
+ assert decoded.created_at.year == 2023
119
+ assert decoded.created_at.month == 6
120
+ assert decoded.created_at.day == 15
121
+
122
+ assert hasattr(decoded, "nested_data")
123
+ assert isinstance(decoded.nested_data, dict)
124
+ assert "timestamp" in decoded.nested_data
125
+ assert isinstance(decoded.nested_data["timestamp"], datetime)
126
+ assert decoded.nested_data["timestamp"].year == 2023
127
+ assert decoded.nested_data["timestamp"].month == 6
128
+ assert decoded.nested_data["timestamp"].day == 16
129
+
130
+
131
+ def test_direct_value_decoding() -> None:
132
+ """
133
+ Test decoding of direct values without type markers
134
+ """
135
+ # Test datetime direct string
136
+ datetime_str = "2023-01-01T00:00:00"
137
+ decoded_datetime = TypePreservationDecoder.decode(datetime_str)
138
+ assert decoded_datetime == datetime_str
139
+
140
+ # Test list with mixed types
141
+ mixed_list = [
142
+ {"__type__": "datetime", "iso": "2023-06-15T10:30:00"},
143
+ 42,
144
+ "plain string",
145
+ ]
146
+ decoded_list = TypePreservationDecoder.decode(mixed_list)
147
+ assert len(decoded_list) == 3
148
+ assert isinstance(decoded_list[0], datetime)
149
+ assert decoded_list[1] == 42
150
+ assert decoded_list[2] == "plain string"
@@ -1,5 +1,7 @@
1
+ import logging
1
2
  from datetime import datetime, timezone, date
2
3
  from decimal import Decimal
4
+ from logging import Logger
3
5
  from typing import Any
4
6
 
5
7
  from compressedfhir.utilities.json_serializers.type_preservation_serializer import (
@@ -58,3 +60,112 @@ def test_nested_complex_data() -> None:
58
60
  deserialized = TypePreservationSerializer.deserialize(serialized)
59
61
 
60
62
  assert isinstance(deserialized["level1"]["level2"]["timestamp"], datetime)
63
+
64
+
65
+ def test_nested_dict() -> None:
66
+ """
67
+ Test serialization of nested dictionaries
68
+ """
69
+ logger: Logger = logging.getLogger(__name__)
70
+ nested_dict = {
71
+ "beneficiary": {"reference": "Patient/1234567890123456703", "type": "Patient"},
72
+ "class": [
73
+ {
74
+ "name": "Aetna Plan",
75
+ "type": {
76
+ "coding": [
77
+ {
78
+ "code": "plan",
79
+ "display": "Plan",
80
+ "system": "http://terminology.hl7.org/CodeSystem/coverage-class",
81
+ }
82
+ ]
83
+ },
84
+ "value": "AE303",
85
+ }
86
+ ],
87
+ "costToBeneficiary": [
88
+ {
89
+ "type": {"text": "Annual Physical Exams NMC - In Network"},
90
+ "valueQuantity": {
91
+ "system": "http://aetna.com/Medicare/CostToBeneficiary/ValueQuantity/code",
92
+ "unit": "$",
93
+ "value": 50.0,
94
+ },
95
+ }
96
+ ],
97
+ "id": "3456789012345670304",
98
+ "identifier": [
99
+ {
100
+ "system": "https://sources.aetna.com/coverage/identifier/membershipid/59",
101
+ "type": {
102
+ "coding": [
103
+ {
104
+ "code": "SN",
105
+ "system": "http://terminology.hl7.org/CodeSystem/v2-0203",
106
+ }
107
+ ]
108
+ },
109
+ "value": "435679010300+AE303+2021-01-01",
110
+ },
111
+ {
112
+ "id": "uuid",
113
+ "system": "https://www.icanbwell.com/uuid",
114
+ "value": "92266603-aa8b-58c6-99bd-326fd1da1896",
115
+ },
116
+ ],
117
+ "meta": {
118
+ "security": [
119
+ {"code": "aetna", "system": "https://www.icanbwell.com/owner"},
120
+ {"code": "aetna", "system": "https://www.icanbwell.com/access"},
121
+ {"code": "aetna", "system": "https://www.icanbwell.com/vendor"},
122
+ {"code": "proa", "system": "https://www.icanbwell.com/connectionType"},
123
+ ],
124
+ "source": "http://mock-server:1080/test_patient_access_transformer/source/4_0_0/Coverage/3456789012345670304",
125
+ },
126
+ "network": "Medicare - MA/NY/NJ - Full Reciprocity",
127
+ "payor": [
128
+ {
129
+ "display": "Aetna",
130
+ "reference": "Organization/6667778889990000015",
131
+ "type": "Organization",
132
+ }
133
+ ],
134
+ "period": {
135
+ "end": datetime.fromisoformat("2021-12-31").date(),
136
+ "start": datetime.fromisoformat("2021-01-01").date(),
137
+ },
138
+ "policyHolder": {"reference": "Patient/1234567890123456703", "type": "Patient"},
139
+ "relationship": {
140
+ "coding": [
141
+ {
142
+ "code": "self",
143
+ "system": "http://terminology.hl7.org/CodeSystem/subscriber-relationship",
144
+ }
145
+ ]
146
+ },
147
+ "resourceType": "Coverage",
148
+ "status": "active",
149
+ "subscriber": {"reference": "Patient/1234567890123456703", "type": "Patient"},
150
+ "subscriberId": "435679010300",
151
+ "type": {
152
+ "coding": [
153
+ {
154
+ "code": "PPO",
155
+ "display": "preferred provider organization policy",
156
+ "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
157
+ }
158
+ ]
159
+ },
160
+ }
161
+
162
+ logger.info("-------- Serialized --------")
163
+ serialized = TypePreservationSerializer.serialize(nested_dict)
164
+ logger.info(serialized)
165
+ logger.info("-------- Deserialized --------")
166
+ deserialized = TypePreservationSerializer.deserialize(serialized)
167
+ logger.info(deserialized)
168
+
169
+ assert isinstance(deserialized["period"]["start"], date)
170
+ assert isinstance(deserialized["period"]["end"], date)
171
+ assert nested_dict == deserialized
@@ -1,63 +1,110 @@
1
+ import logging
2
+ from collections import OrderedDict
1
3
  from datetime import datetime, date
2
4
  from decimal import Decimal
3
- from typing import Any, Dict, Callable
5
+ from logging import Logger
6
+ from typing import Any, Dict, Callable, Optional, Union, cast, List
4
7
 
5
8
 
6
9
  class TypePreservationDecoder:
7
10
  """
8
- Advanced JSON decoder for complex type reconstruction
11
+ Advanced JSON decoder for complex type reconstruction with nested type support
9
12
  """
10
13
 
11
14
  @classmethod
12
15
  def decode(
13
16
  cls,
14
- dct: Dict[str, Any],
15
- custom_decoders: Dict[str, Callable[[Any], Any]] | None = None,
17
+ dct: Union[str, Dict[str, Any], List[Any]],
18
+ custom_decoders: Optional[Dict[str, Callable[[Any], Any]]] = None,
19
+ use_ordered_dict: bool = True,
16
20
  ) -> Any:
17
21
  """
18
- Decode complex types
22
+ Decode complex types, including nested datetime fields
19
23
 
20
24
  Args:
21
25
  dct: Dictionary to decode
22
26
  custom_decoders: Optional additional custom decoders
27
+ use_ordered_dict: Flag to control whether to use OrderedDict or not
23
28
 
24
29
  Returns:
25
30
  Reconstructed object or original dictionary
26
31
  """
27
- # Default decoders for built-in types
32
+ logger: Logger = logging.getLogger(__name__)
33
+
34
+ # Default decoders for built-in types with nested support
35
+ def datetime_decoder(d: Union[str, Dict[str, Any]]) -> datetime:
36
+ if isinstance(d, str):
37
+ return datetime.fromisoformat(d)
38
+ elif isinstance(d, dict) and "iso" in d:
39
+ return datetime.fromisoformat(d["iso"])
40
+ return cast(datetime, d)
41
+
42
+ def date_decoder(d: Union[str, Dict[str, Any]]) -> date:
43
+ if isinstance(d, str):
44
+ return date.fromisoformat(d)
45
+ elif isinstance(d, dict) and "iso" in d:
46
+ return date.fromisoformat(d["iso"])
47
+ return cast(date, d)
48
+
28
49
  default_decoders: Dict[str, Callable[[Any], Any]] = {
29
- "datetime": lambda d: datetime.fromisoformat(d["iso"]),
30
- "date": lambda d: date.fromisoformat(d["iso"]),
31
- "decimal": lambda d: Decimal(d["value"]),
32
- "complex": lambda d: complex(d["real"], d["imag"]),
33
- "bytes": lambda d: d["value"].encode("latin-1"),
34
- "set": lambda d: set(d["values"]),
50
+ "datetime": datetime_decoder,
51
+ "date": date_decoder,
52
+ "decimal": lambda d: Decimal(d["value"] if isinstance(d, dict) else d),
53
+ "complex": lambda d: complex(d["real"], d["imag"])
54
+ if isinstance(d, dict)
55
+ else d,
56
+ "bytes": lambda d: d["value"].encode("latin-1")
57
+ if isinstance(d, dict)
58
+ else d,
59
+ "set": lambda d: set(d["values"]) if isinstance(d, dict) else d,
35
60
  }
36
61
 
37
62
  # Merge custom decoders with default decoders
38
63
  decoders = {**default_decoders, **(custom_decoders or {})}
39
64
 
40
- # Check for type marker
41
- if "__type__" in dct:
42
- type_name = dct["__type__"]
43
-
44
- # Handle built-in type decoders
45
- if type_name in decoders:
46
- return decoders[type_name](dct)
47
-
48
- # Handle custom object reconstruction
49
- if "__module__" in dct and "attributes" in dct:
50
- try:
51
- # Dynamically import the class
52
- module = __import__(dct["__module__"], fromlist=[type_name])
53
- cls_ = getattr(module, type_name)
54
-
55
- # Create instance and set attributes
56
- obj = cls_.__new__(cls_)
57
- obj.__dict__.update(dct["attributes"])
58
- return obj
59
- except (ImportError, AttributeError) as e:
60
- print(f"Could not reconstruct {type_name}: {e}")
61
- return dct
62
-
63
- return dct
65
+ # Recursively decode nested structures
66
+ def recursive_decode(value: Any) -> Any:
67
+ if isinstance(value, dict):
68
+ # Check for type marker in the dictionary
69
+ if "__type__" in value:
70
+ type_name = value["__type__"]
71
+
72
+ # Handle built-in type decoders
73
+ if type_name in decoders:
74
+ return decoders[type_name](value)
75
+
76
+ # Handle custom object reconstruction
77
+ if "__module__" in value and "attributes" in value:
78
+ try:
79
+ # Dynamically import the class
80
+ module = __import__(
81
+ value["__module__"], fromlist=[type_name]
82
+ )
83
+ cls_ = getattr(module, type_name)
84
+
85
+ # Create instance and set attributes with recursive decoding
86
+ obj = cls_.__new__(cls_)
87
+ obj.__dict__.update(
88
+ {
89
+ k: recursive_decode(v)
90
+ for k, v in value["attributes"].items()
91
+ }
92
+ )
93
+ return obj
94
+ except (ImportError, AttributeError) as e:
95
+ logger.error(f"Could not reconstruct {type_name}: {e}")
96
+ return value
97
+
98
+ # Recursively decode dictionary values
99
+ # Conditionally use OrderedDict or regular dict
100
+ dict_type = OrderedDict if use_ordered_dict else dict
101
+ return dict_type((k, recursive_decode(v)) for k, v in value.items())
102
+
103
+ # Recursively decode list or tuple
104
+ elif isinstance(value, (list, tuple)):
105
+ return type(value)(recursive_decode(item) for item in value)
106
+
107
+ return value
108
+
109
+ # Start recursive decoding
110
+ return recursive_decode(dct)
@@ -26,7 +26,9 @@ class TypePreservationSerializer:
26
26
  Returns:
27
27
  JSON string representation
28
28
  """
29
- return json.dumps(data, cls=TypePreservationEncoder, indent=2, **kwargs)
29
+ return json.dumps(
30
+ data, cls=TypePreservationEncoder, separators=(",", ":"), **kwargs
31
+ )
30
32
 
31
33
  @classmethod
32
34
  def deserialize(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: compressedfhir
3
- Version: 1.0.3
3
+ Version: 1.0.5
4
4
  Summary: Stores FHIR JSON resources in compressed form in memory
5
5
  Home-page: https://github.com/icanbwell/compressed-fhir
6
6
  Author: Imran Qureshi
@@ -11,16 +11,16 @@ compressedfhir/fhir/fhir_bundle_entry_search.py,sha256=uYVJxuNN3gt3Q6BZ5FhRs47x7
11
11
  compressedfhir/fhir/fhir_identifier.py,sha256=tA_nmhBaYHu5zjJdE0IWMFEF8lrIPV3_nu-yairiIKw,2711
12
12
  compressedfhir/fhir/fhir_link.py,sha256=jf2RrwmsPrKW3saP77y42xVqI0xwHFYXxm6YHQJk7gU,1922
13
13
  compressedfhir/fhir/fhir_meta.py,sha256=vNI4O6SoG4hJRHyd-bJ_QnYFTfBHyR3UA6h21ByQmWo,1669
14
- compressedfhir/fhir/fhir_resource.py,sha256=GIz0g8O-Nw9Av8M5wYRoRY4FS2kEk2Nb03RPSeDYUqo,5588
14
+ compressedfhir/fhir/fhir_resource.py,sha256=5v_xcAUCFcqzQodT8uiw292NUG86gWJ4UbyhY2Vy79c,5084
15
15
  compressedfhir/fhir/fhir_resource_list.py,sha256=qlAAwWWphtFicBxPG8iriz2eOHGcrWJk5kGThmvkbPE,4480
16
- compressedfhir/fhir/fhir_resource_map.py,sha256=6Zt_K8KVolS-lgT_Ztu_6YxNo8BXhweQfWO-QFriInA,6588
16
+ compressedfhir/fhir/fhir_resource_map.py,sha256=XFJ0o5_kLUeHYKp1q_Bxsoyp2-rLX7P4c9FwQ7YfGWM,6571
17
17
  compressedfhir/fhir/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  compressedfhir/fhir/test/test_bundle_entry.py,sha256=Ki2sSu1V1WZkAM6UTCghtzjvjYYI8UcF6AXnx8FWlMI,5115
19
19
  compressedfhir/fhir/test/test_bundle_entry_list.py,sha256=KtMrbQYezdEw9FJbBzwSePdJK2R9P03mSRfo59T-6iM,6041
20
20
  compressedfhir/fhir/test/test_bundle_entry_request.py,sha256=9bN3Vt9BAXPLjH7FFt_MYSdanFJzWk9HbA0C9kZxPXY,2853
21
21
  compressedfhir/fhir/test/test_bundle_entry_response.py,sha256=jk5nUi07_q-yz-qz2YR86vU91e3DVxc2cptrS6tsCco,2539
22
22
  compressedfhir/fhir/test/test_fhir_bundle.py,sha256=Kt1IpxEnUuPOJBDWsdy4cC7kR3FR-uPOf7PB9ejJ7ZM,8700
23
- compressedfhir/fhir/test/test_fhir_resource.py,sha256=4Fl6QaqjW4CsYqkxVj2WRXITv_MeozUIrZgN4bMBGIw,8002
23
+ compressedfhir/fhir/test/test_fhir_resource.py,sha256=nsSLs-sKDaYpoTVyXuBNnKJ0-somxDNX368lpTf3HUw,3828
24
24
  compressedfhir/fhir/test/test_fhir_resource_list.py,sha256=SrSPJ1yWU4UgMUCht6JwgKh2Y5JeTS4-Wky0kWZOXH8,5664
25
25
  compressedfhir/fhir/test/test_fhir_resource_map.py,sha256=jtQ5fq_jhmFfhHGyK5mdiwIQiO-Sfp2eG9mco_Tr9Qk,10995
26
26
  compressedfhir/utilities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -28,19 +28,19 @@ compressedfhir/utilities/fhir_json_encoder.py,sha256=hn-ZuDrTEdYZmILk_5_k4R72PQB
28
28
  compressedfhir/utilities/json_helpers.py,sha256=lEiPapLN0p-kLu6PFm-h971ieXRxwPB2M-8FCZ2Buo8,5642
29
29
  compressedfhir/utilities/compressed_dict/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  compressedfhir/utilities/compressed_dict/v1/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- compressedfhir/utilities/compressed_dict/v1/compressed_dict.py,sha256=sf8mGBdvYpjcMfVSWUVFGTiEi_pimutwCWyfKbAY2OU,21314
31
+ compressedfhir/utilities/compressed_dict/v1/compressed_dict.py,sha256=C2mblG2P8qx0XWpDJO-7OGBqyQRTp0WelaUbwopI7qc,22049
32
32
  compressedfhir/utilities/compressed_dict/v1/compressed_dict_access_error.py,sha256=xuwED0KGZcQORIcZRfi--5CdXplHJ5vYLBUqpbDi344,132
33
33
  compressedfhir/utilities/compressed_dict/v1/compressed_dict_storage_mode.py,sha256=mEdtJjPX2I9DqP0Ly_VsZZWhEMNTI1psqQ8iJtUQ2oE,1412
34
34
  compressedfhir/utilities/compressed_dict/v1/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- compressedfhir/utilities/compressed_dict/v1/test/test_compressed_dict.py,sha256=7AsOX1Nw7Woo9C7OzdBXMXFQhgEBAZZ8py1aHfFh-4k,11970
35
+ compressedfhir/utilities/compressed_dict/v1/test/test_compressed_dict.py,sha256=5yUnjkmP3A4dSPzDXY3u1YBQ8BxCANdtCF9uGF1T9i4,15840
36
36
  compressedfhir/utilities/json_serializers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- compressedfhir/utilities/json_serializers/type_preservation_decoder.py,sha256=vLGSuyN7FXlpoJwpBvCPf27RpA1DmmKQ0BO_xPOLJiw,2135
37
+ compressedfhir/utilities/json_serializers/type_preservation_decoder.py,sha256=Af2ZsLZiUF9kUhRvkV7i6Ctf_OtTND_lb5PezHtolJU,4382
38
38
  compressedfhir/utilities/json_serializers/type_preservation_encoder.py,sha256=f7RL67l7QtDbijCPq1ki6axrLte1vH--bi1AsN7Y3yk,1646
39
- compressedfhir/utilities/json_serializers/type_preservation_serializer.py,sha256=jhut-eqVMhAYnAVA9GOH8moJBn20pqA7-MBqsW-JXeY,1488
39
+ compressedfhir/utilities/json_serializers/type_preservation_serializer.py,sha256=cE1ka2RxKy_8P0xhgqvPyWqJ3C0Br-aqIHP9BPkCg7A,1523
40
40
  compressedfhir/utilities/json_serializers/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- compressedfhir/utilities/json_serializers/test/test_type_preservation_decoder.py,sha256=sVdZoZ6u8luyjmBLae_6Bk8lsYTaxBNU-e-P-nWyVMk,2329
41
+ compressedfhir/utilities/json_serializers/test/test_type_preservation_decoder.py,sha256=GQotwYQJe9VZQotvLWmQWMkSIBne53bolmgflBoR7DU,4752
42
42
  compressedfhir/utilities/json_serializers/test/test_type_preservation_encoder.py,sha256=O4VczBdsJF35WozZiwSdJ8638qDn01JQsai2wTXu5Vo,1737
43
- compressedfhir/utilities/json_serializers/test/test_type_preservation_serializer.py,sha256=RwshpoLN-f3bXmT1QhwWANpndGMxwtyu9O-1SMMwmgQ,1985
43
+ compressedfhir/utilities/json_serializers/test/test_type_preservation_serializer.py,sha256=dTYdgI1wMgWU0DJCNJlbMmsnhr-Q_2SPXeydLsn70rA,6043
44
44
  compressedfhir/utilities/ordered_dict_to_dict_converter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  compressedfhir/utilities/ordered_dict_to_dict_converter/ordered_dict_to_dict_converter.py,sha256=CMerJQD7O0vMyGtUp1rKSerZA1tDZeY5GTQT3AykL4w,831
46
46
  compressedfhir/utilities/string_compressor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -51,9 +51,9 @@ compressedfhir/utilities/string_compressor/v1/test/test_string_compressor.py,sha
51
51
  compressedfhir/utilities/test/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
52
  compressedfhir/utilities/test/test_fhir_json_encoder.py,sha256=6pbNmZp5eBWY66bHjgjm_pZVhs5HDKP8hCGnwNFzpEw,5171
53
53
  compressedfhir/utilities/test/test_json_helpers.py,sha256=V0R9oHDQAs0m0012niEz50sHJxMSUQvA3km7kK8HgjE,3860
54
- compressedfhir-1.0.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
54
+ compressedfhir-1.0.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
55
55
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- compressedfhir-1.0.3.dist-info/METADATA,sha256=4HYsDmJhx91m8f7HbhReC5tL6mkP3Z7a_ZqDbzIEgaI,3456
57
- compressedfhir-1.0.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
58
- compressedfhir-1.0.3.dist-info/top_level.txt,sha256=YMKdvBBdiCzFbpI9fG8BUDjaRd-f4R0qAvUoVETpoWw,21
59
- compressedfhir-1.0.3.dist-info/RECORD,,
56
+ compressedfhir-1.0.5.dist-info/METADATA,sha256=jitzP2jc8_vqD1C9RYDNyetYivS_DOHuUplgJZy8t20,3456
57
+ compressedfhir-1.0.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
58
+ compressedfhir-1.0.5.dist-info/top_level.txt,sha256=YMKdvBBdiCzFbpI9fG8BUDjaRd-f4R0qAvUoVETpoWw,21
59
+ compressedfhir-1.0.5.dist-info/RECORD,,