compressedfhir 3.0.2__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.
- compressedfhir/__init__.py +0 -0
- compressedfhir/fhir/__init__.py +0 -0
- compressedfhir/fhir/base_resource_list.py +165 -0
- compressedfhir/fhir/fhir_bundle.py +295 -0
- compressedfhir/fhir/fhir_bundle_entry.py +240 -0
- compressedfhir/fhir/fhir_bundle_entry_list.py +97 -0
- compressedfhir/fhir/fhir_bundle_entry_request.py +73 -0
- compressedfhir/fhir/fhir_bundle_entry_response.py +67 -0
- compressedfhir/fhir/fhir_bundle_entry_search.py +75 -0
- compressedfhir/fhir/fhir_identifier.py +84 -0
- compressedfhir/fhir/fhir_link.py +63 -0
- compressedfhir/fhir/fhir_meta.py +47 -0
- compressedfhir/fhir/fhir_resource.py +170 -0
- compressedfhir/fhir/fhir_resource_list.py +149 -0
- compressedfhir/fhir/fhir_resource_map.py +193 -0
- compressedfhir/fhir/test/__init__.py +0 -0
- compressedfhir/fhir/test/test_bundle_entry.py +129 -0
- compressedfhir/fhir/test/test_bundle_entry_list.py +187 -0
- compressedfhir/fhir/test/test_bundle_entry_request.py +74 -0
- compressedfhir/fhir/test/test_bundle_entry_response.py +65 -0
- compressedfhir/fhir/test/test_fhir_bundle.py +245 -0
- compressedfhir/fhir/test/test_fhir_resource.py +104 -0
- compressedfhir/fhir/test/test_fhir_resource_list.py +160 -0
- compressedfhir/fhir/test/test_fhir_resource_map.py +293 -0
- compressedfhir/py.typed +0 -0
- compressedfhir/utilities/__init__.py +0 -0
- compressedfhir/utilities/compressed_dict/__init__.py +0 -0
- compressedfhir/utilities/compressed_dict/v1/__init__.py +0 -0
- compressedfhir/utilities/compressed_dict/v1/compressed_dict.py +701 -0
- compressedfhir/utilities/compressed_dict/v1/compressed_dict_access_error.py +2 -0
- compressedfhir/utilities/compressed_dict/v1/compressed_dict_storage_mode.py +50 -0
- compressedfhir/utilities/compressed_dict/v1/test/__init__.py +0 -0
- compressedfhir/utilities/compressed_dict/v1/test/test_compressed_dict.py +467 -0
- compressedfhir/utilities/fhir_json_encoder.py +71 -0
- compressedfhir/utilities/json_helpers.py +181 -0
- compressedfhir/utilities/json_serializers/__init__.py +0 -0
- compressedfhir/utilities/json_serializers/test/__init__.py +0 -0
- compressedfhir/utilities/json_serializers/test/test_type_preservation_decoder.py +165 -0
- compressedfhir/utilities/json_serializers/test/test_type_preservation_encoder.py +71 -0
- compressedfhir/utilities/json_serializers/test/test_type_preservation_serializer.py +197 -0
- compressedfhir/utilities/json_serializers/type_preservation_decoder.py +135 -0
- compressedfhir/utilities/json_serializers/type_preservation_encoder.py +55 -0
- compressedfhir/utilities/json_serializers/type_preservation_serializer.py +57 -0
- compressedfhir/utilities/ordered_dict_to_dict_converter/__init__.py +0 -0
- compressedfhir/utilities/ordered_dict_to_dict_converter/ordered_dict_to_dict_converter.py +24 -0
- compressedfhir/utilities/string_compressor/__init__.py +0 -0
- compressedfhir/utilities/string_compressor/v1/__init__.py +0 -0
- compressedfhir/utilities/string_compressor/v1/string_compressor.py +99 -0
- compressedfhir/utilities/string_compressor/v1/test/__init__.py +0 -0
- compressedfhir/utilities/string_compressor/v1/test/test_string_compressor.py +189 -0
- compressedfhir/utilities/test/__init__.py +0 -0
- compressedfhir/utilities/test/test_fhir_json_encoder.py +177 -0
- compressedfhir/utilities/test/test_json_helpers.py +99 -0
- compressedfhir-3.0.2.dist-info/METADATA +139 -0
- compressedfhir-3.0.2.dist-info/RECORD +59 -0
- compressedfhir-3.0.2.dist-info/WHEEL +5 -0
- compressedfhir-3.0.2.dist-info/licenses/LICENSE +201 -0
- compressedfhir-3.0.2.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, List, cast, Union, Optional, OrderedDict, overload
|
|
3
|
+
from datetime import datetime, date
|
|
4
|
+
|
|
5
|
+
import orjson
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FhirClientJsonHelpers:
|
|
9
|
+
def __init__(self) -> None:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def json_serial(obj: Any) -> str:
|
|
14
|
+
"""JSON serializer for objects not serializable by default json code"""
|
|
15
|
+
|
|
16
|
+
# https://stackoverflow.com/questions/11875770/how-to-overcome-datetime-datetime-not-json-serializable
|
|
17
|
+
if isinstance(obj, (datetime, date)):
|
|
18
|
+
return obj.isoformat()
|
|
19
|
+
return str(obj)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def remove_empty_elements(
|
|
23
|
+
d: List[Dict[str, Any]] | Dict[str, Any],
|
|
24
|
+
) -> List[Dict[str, Any]] | Dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Recursively remove empty lists, empty dicts, or None elements from a dictionary
|
|
27
|
+
or a list of dictionaries
|
|
28
|
+
:param d: dictionary or list of dictionaries
|
|
29
|
+
:return: dictionary or list of dictionaries
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def empty(x: Any) -> bool:
|
|
33
|
+
# Check if the input is None, an empty list, an empty dict, or an empty string
|
|
34
|
+
return (
|
|
35
|
+
x is None
|
|
36
|
+
or x == []
|
|
37
|
+
or x == {}
|
|
38
|
+
or (isinstance(x, str) and x.strip() == "")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if not isinstance(d, (dict, list)):
|
|
42
|
+
return d
|
|
43
|
+
elif isinstance(d, list):
|
|
44
|
+
return [
|
|
45
|
+
cast(Dict[str, Any], v)
|
|
46
|
+
for v in (FhirClientJsonHelpers.remove_empty_elements(v) for v in d)
|
|
47
|
+
if not empty(v)
|
|
48
|
+
]
|
|
49
|
+
else:
|
|
50
|
+
return {
|
|
51
|
+
k: v
|
|
52
|
+
for k, v in (
|
|
53
|
+
(k, FhirClientJsonHelpers.remove_empty_elements(v))
|
|
54
|
+
for k, v in d.items()
|
|
55
|
+
)
|
|
56
|
+
if not empty(v)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
@overload
|
|
61
|
+
def remove_empty_elements_from_ordered_dict(
|
|
62
|
+
d: List[OrderedDict[str, Any]],
|
|
63
|
+
) -> List[OrderedDict[str, Any]]: ...
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
@overload
|
|
67
|
+
def remove_empty_elements_from_ordered_dict(
|
|
68
|
+
d: OrderedDict[str, Any],
|
|
69
|
+
) -> OrderedDict[str, Any]: ...
|
|
70
|
+
|
|
71
|
+
@staticmethod
|
|
72
|
+
def remove_empty_elements_from_ordered_dict(
|
|
73
|
+
d: List[OrderedDict[str, Any]] | OrderedDict[str, Any],
|
|
74
|
+
) -> List[OrderedDict[str, Any]] | OrderedDict[str, Any]:
|
|
75
|
+
"""
|
|
76
|
+
Recursively remove empty lists, empty dicts, or None elements from a dictionary
|
|
77
|
+
or a list of dictionaries
|
|
78
|
+
:param d: dictionary or list of dictionaries
|
|
79
|
+
:return: dictionary or list of dictionaries
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def empty(x: Any) -> bool:
|
|
83
|
+
# Check if the input is None, an empty list, an empty dict, or an empty string
|
|
84
|
+
return (
|
|
85
|
+
x is None
|
|
86
|
+
or x == []
|
|
87
|
+
or x == {}
|
|
88
|
+
or (isinstance(x, str) and x.strip() == "")
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if not isinstance(d, (OrderedDict, list, Dict)):
|
|
92
|
+
return d
|
|
93
|
+
elif isinstance(d, list):
|
|
94
|
+
return [
|
|
95
|
+
v
|
|
96
|
+
for v in (
|
|
97
|
+
FhirClientJsonHelpers.remove_empty_elements_from_ordered_dict(v)
|
|
98
|
+
for v in d
|
|
99
|
+
)
|
|
100
|
+
if not empty(v)
|
|
101
|
+
]
|
|
102
|
+
else:
|
|
103
|
+
return OrderedDict[str, Any](
|
|
104
|
+
{
|
|
105
|
+
k: v
|
|
106
|
+
for k, v in (
|
|
107
|
+
(
|
|
108
|
+
k,
|
|
109
|
+
FhirClientJsonHelpers.remove_empty_elements_from_ordered_dict(
|
|
110
|
+
v
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
for k, v in d.items()
|
|
114
|
+
)
|
|
115
|
+
if not empty(v)
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def convert_dict_to_fhir_json(dict_: Dict[str, Any]) -> str:
|
|
121
|
+
"""
|
|
122
|
+
Returns dictionary as json string
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
:return:
|
|
126
|
+
"""
|
|
127
|
+
instance_variables: Dict[str, Any] = cast(
|
|
128
|
+
Dict[str, Any], FhirClientJsonHelpers.remove_empty_elements(dict_)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
instance_variables_text: str = json.dumps(
|
|
132
|
+
instance_variables, default=FhirClientJsonHelpers.json_serial
|
|
133
|
+
)
|
|
134
|
+
return instance_variables_text
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def orjson_dumps(
|
|
138
|
+
obj: Any, indent: Optional[int] = None, sort_keys: bool = False
|
|
139
|
+
) -> str:
|
|
140
|
+
"""
|
|
141
|
+
Wrapper for orjson.dumps() to mimic json.dumps() behavior
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
obj: Object to serialize
|
|
145
|
+
indent: Optional indentation (note: orjson has limited indent support)
|
|
146
|
+
sort_keys: Whether to sort dictionary keys
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
JSON string
|
|
150
|
+
"""
|
|
151
|
+
# Serialization options
|
|
152
|
+
options = 0
|
|
153
|
+
|
|
154
|
+
# Handle sorting keys
|
|
155
|
+
if sort_keys:
|
|
156
|
+
options |= orjson.OPT_SORT_KEYS
|
|
157
|
+
|
|
158
|
+
# Handle indentation (limited support)
|
|
159
|
+
if indent is not None:
|
|
160
|
+
options |= orjson.OPT_INDENT_2 # Fixed indentation
|
|
161
|
+
|
|
162
|
+
# Serialize to bytes
|
|
163
|
+
json_bytes = orjson.dumps(obj, option=options)
|
|
164
|
+
|
|
165
|
+
# Convert bytes to string
|
|
166
|
+
return json_bytes.decode("utf-8")
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def orjson_loads(json_input: Union[str, bytes]) -> Any:
|
|
170
|
+
"""
|
|
171
|
+
Safely load JSON with type flexibility
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
# Converts input to appropriate type if needed
|
|
175
|
+
if isinstance(json_input, str):
|
|
176
|
+
json_input = json_input.encode("utf-8")
|
|
177
|
+
|
|
178
|
+
return orjson.loads(json_input)
|
|
179
|
+
except (orjson.JSONDecodeError, TypeError) as e:
|
|
180
|
+
print(f"JSON Parsing Error: {e}")
|
|
181
|
+
return None
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from datetime import datetime, date, time
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from typing import Type, Any, Dict, Optional
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from compressedfhir.utilities.json_serializers.type_preservation_decoder import (
|
|
7
|
+
TypePreservationDecoder,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCustomObject:
|
|
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
|
+
):
|
|
19
|
+
self.name: str = name
|
|
20
|
+
self.value: int = value
|
|
21
|
+
self.created_at: Optional[datetime] = created_at
|
|
22
|
+
self.nested_data: Optional[Dict[str, Any]] = nested_data
|
|
23
|
+
|
|
24
|
+
def __eq__(self, other: Any) -> bool:
|
|
25
|
+
if not isinstance(other, TestCustomObject):
|
|
26
|
+
return False
|
|
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
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize(
|
|
36
|
+
"input_type, input_dict, expected_type",
|
|
37
|
+
[
|
|
38
|
+
(
|
|
39
|
+
"datetime",
|
|
40
|
+
{"__type__": "datetime", "iso": "2023-01-01T00:00:00+00:00"},
|
|
41
|
+
datetime,
|
|
42
|
+
),
|
|
43
|
+
(
|
|
44
|
+
"datetime",
|
|
45
|
+
{
|
|
46
|
+
"__type__": "datetime",
|
|
47
|
+
"iso": "2023-01-01T00:00:00-08:00",
|
|
48
|
+
"tzinfo": "Pacific/Honolulu",
|
|
49
|
+
},
|
|
50
|
+
datetime,
|
|
51
|
+
),
|
|
52
|
+
("date", {"__type__": "date", "iso": "2023-01-01"}, date),
|
|
53
|
+
("time", {"__type__": "time", "iso": "14:30:15"}, time),
|
|
54
|
+
(
|
|
55
|
+
"time",
|
|
56
|
+
{"__type__": "time", "iso": "14:30:15", "tzinfo": "Pacific/Honolulu"},
|
|
57
|
+
time,
|
|
58
|
+
),
|
|
59
|
+
("decimal", {"__type__": "decimal", "value": "3.14"}, Decimal),
|
|
60
|
+
("complex", {"__type__": "complex", "real": 3, "imag": 4}, complex),
|
|
61
|
+
("bytes", {"__type__": "bytes", "value": "test"}, bytes),
|
|
62
|
+
("set", {"__type__": "set", "values": [1, 2, 3]}, set),
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
def test_complex_type_decoding(
|
|
66
|
+
input_type: str, input_dict: Dict[str, Any], expected_type: Type[Any]
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Test decoding of various complex types
|
|
70
|
+
"""
|
|
71
|
+
decoded = TypePreservationDecoder.decode(input_dict)
|
|
72
|
+
assert isinstance(decoded, expected_type)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_custom_object_decoding() -> None:
|
|
76
|
+
"""
|
|
77
|
+
Test decoding of custom objects
|
|
78
|
+
"""
|
|
79
|
+
custom_obj_dict = {
|
|
80
|
+
"__type__": "TestCustomObject",
|
|
81
|
+
"__module__": __name__,
|
|
82
|
+
"attributes": {"name": "test", "value": 42},
|
|
83
|
+
}
|
|
84
|
+
decoded = TypePreservationDecoder.decode(custom_obj_dict)
|
|
85
|
+
assert isinstance(decoded, TestCustomObject)
|
|
86
|
+
assert decoded.name == "test"
|
|
87
|
+
assert decoded.value == 42
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_custom_decoder() -> None:
|
|
91
|
+
"""
|
|
92
|
+
Test custom decoder functionality
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def custom_decoder(data: Dict[str, Any]) -> Any:
|
|
96
|
+
if data.get("__type__") == "special_type":
|
|
97
|
+
return f"Decoded: {data['value']}"
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
special_dict = {"__type__": "special_type", "value": "test"}
|
|
101
|
+
decoded = TypePreservationDecoder.decode(
|
|
102
|
+
special_dict, custom_decoders={"special_type": custom_decoder}
|
|
103
|
+
)
|
|
104
|
+
assert decoded == "Decoded: test"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_nested_datetime_decoding() -> None:
|
|
108
|
+
"""
|
|
109
|
+
Test decoding of nested datetime fields
|
|
110
|
+
"""
|
|
111
|
+
nested_datetime_dict = {
|
|
112
|
+
"__type__": "TestCustomObject",
|
|
113
|
+
"__module__": __name__,
|
|
114
|
+
"attributes": {
|
|
115
|
+
"name": "test",
|
|
116
|
+
"value": 42,
|
|
117
|
+
"created_at": {"__type__": "datetime", "iso": "2023-06-15T10:30:00"},
|
|
118
|
+
"nested_data": {
|
|
119
|
+
"timestamp": {"__type__": "datetime", "iso": "2023-06-16T15:45:00"}
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
decoded: TestCustomObject = TypePreservationDecoder.decode(nested_datetime_dict)
|
|
125
|
+
|
|
126
|
+
assert isinstance(decoded, TestCustomObject)
|
|
127
|
+
assert decoded.name == "test"
|
|
128
|
+
assert decoded.value == 42
|
|
129
|
+
|
|
130
|
+
# Check nested datetime fields
|
|
131
|
+
assert hasattr(decoded, "created_at")
|
|
132
|
+
assert isinstance(decoded.created_at, datetime)
|
|
133
|
+
assert decoded.created_at.year == 2023
|
|
134
|
+
assert decoded.created_at.month == 6
|
|
135
|
+
assert decoded.created_at.day == 15
|
|
136
|
+
|
|
137
|
+
assert hasattr(decoded, "nested_data")
|
|
138
|
+
assert isinstance(decoded.nested_data, dict)
|
|
139
|
+
assert "timestamp" in decoded.nested_data
|
|
140
|
+
assert isinstance(decoded.nested_data["timestamp"], datetime)
|
|
141
|
+
assert decoded.nested_data["timestamp"].year == 2023
|
|
142
|
+
assert decoded.nested_data["timestamp"].month == 6
|
|
143
|
+
assert decoded.nested_data["timestamp"].day == 16
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_direct_value_decoding() -> None:
|
|
147
|
+
"""
|
|
148
|
+
Test decoding of direct values without type markers
|
|
149
|
+
"""
|
|
150
|
+
# Test datetime direct string
|
|
151
|
+
datetime_str = "2023-01-01T00:00:00"
|
|
152
|
+
decoded_datetime = TypePreservationDecoder.decode(datetime_str)
|
|
153
|
+
assert decoded_datetime == datetime_str
|
|
154
|
+
|
|
155
|
+
# Test list with mixed types
|
|
156
|
+
mixed_list = [
|
|
157
|
+
{"__type__": "datetime", "iso": "2023-06-15T10:30:00"},
|
|
158
|
+
42,
|
|
159
|
+
"plain string",
|
|
160
|
+
]
|
|
161
|
+
decoded_list = TypePreservationDecoder.decode(mixed_list)
|
|
162
|
+
assert len(decoded_list) == 3
|
|
163
|
+
assert isinstance(decoded_list[0], datetime)
|
|
164
|
+
assert decoded_list[1] == 42
|
|
165
|
+
assert decoded_list[2] == "plain string"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from datetime import datetime, timezone, date, time
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from logging import Logger
|
|
5
|
+
from typing import Type, Any
|
|
6
|
+
from zoneinfo import ZoneInfo
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from compressedfhir.utilities.json_serializers.type_preservation_encoder import (
|
|
11
|
+
TypePreservationEncoder,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestCustomObject:
|
|
16
|
+
def __init__(self, name: str, value: int):
|
|
17
|
+
self.name: str = name
|
|
18
|
+
self.value: int = value
|
|
19
|
+
|
|
20
|
+
def __eq__(self, other: Any) -> bool:
|
|
21
|
+
if not isinstance(other, TestCustomObject):
|
|
22
|
+
return False
|
|
23
|
+
return self.name == other.name and self.value == other.value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize(
|
|
27
|
+
"input_type, input_value, expected_type",
|
|
28
|
+
[
|
|
29
|
+
(datetime, datetime(2023, 1, 1, tzinfo=timezone.utc), "datetime"),
|
|
30
|
+
(
|
|
31
|
+
datetime,
|
|
32
|
+
datetime(2023, 1, 1, tzinfo=ZoneInfo("Pacific/Honolulu")),
|
|
33
|
+
"datetime",
|
|
34
|
+
),
|
|
35
|
+
(date, date(2023, 1, 1), "date"),
|
|
36
|
+
(time, time(14, 30, 15), "time"),
|
|
37
|
+
(time, time(14, 30, 15, tzinfo=ZoneInfo("Pacific/Honolulu")), "time"),
|
|
38
|
+
(Decimal, Decimal("3.14"), "decimal"),
|
|
39
|
+
(complex, 3 + 4j, "complex"),
|
|
40
|
+
(bytes, b"test", "bytes"),
|
|
41
|
+
(set, {1, 2, 3}, "set"),
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
def test_complex_type_serialization(
|
|
45
|
+
input_type: Type[Any], input_value: Any, expected_type: str
|
|
46
|
+
) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Test serialization of various complex types
|
|
49
|
+
"""
|
|
50
|
+
logger: Logger = logging.getLogger(__name__)
|
|
51
|
+
encoder = TypePreservationEncoder()
|
|
52
|
+
serialized = encoder.default(input_value)
|
|
53
|
+
|
|
54
|
+
logger.info(serialized)
|
|
55
|
+
assert isinstance(serialized, dict)
|
|
56
|
+
assert serialized.get("__type__") == expected_type
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# noinspection PyMethodMayBeStatic
|
|
60
|
+
def test_custom_object_serialization() -> None:
|
|
61
|
+
"""
|
|
62
|
+
Test serialization of custom objects
|
|
63
|
+
"""
|
|
64
|
+
custom_obj = TestCustomObject("test", 42)
|
|
65
|
+
encoder = TypePreservationEncoder()
|
|
66
|
+
serialized = encoder.default(custom_obj)
|
|
67
|
+
|
|
68
|
+
assert isinstance(serialized, dict)
|
|
69
|
+
assert serialized.get("__type__") == "TestCustomObject"
|
|
70
|
+
assert serialized.get("__module__") == __name__
|
|
71
|
+
assert "attributes" in serialized
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections import OrderedDict
|
|
3
|
+
from datetime import datetime, timezone, date, time
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
from logging import Logger
|
|
6
|
+
from typing import Any
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
|
|
9
|
+
from compressedfhir.utilities.json_serializers.type_preservation_serializer import (
|
|
10
|
+
TypePreservationSerializer,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestCustomObject:
|
|
15
|
+
def __init__(self, name: str, value: int):
|
|
16
|
+
self.name: str = name
|
|
17
|
+
self.value: int = value
|
|
18
|
+
|
|
19
|
+
def __eq__(self, other: Any) -> bool:
|
|
20
|
+
if not isinstance(other, TestCustomObject):
|
|
21
|
+
return False
|
|
22
|
+
return self.name == other.name and self.value == other.value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_complex_data_serialization() -> None:
|
|
26
|
+
"""
|
|
27
|
+
Test serialization and deserialization of complex data
|
|
28
|
+
"""
|
|
29
|
+
complex_data = {
|
|
30
|
+
"timestamp_no_tz": datetime.now(),
|
|
31
|
+
"timestamp": datetime.now(timezone.utc),
|
|
32
|
+
"timestamp_pst": datetime.now(ZoneInfo("Pacific/Honolulu")),
|
|
33
|
+
"today": date.today(),
|
|
34
|
+
"my_time": time(14, 30, 15),
|
|
35
|
+
"my_time_pst": time(14, 30, 15, tzinfo=ZoneInfo("Pacific/Honolulu")),
|
|
36
|
+
"precise_value": Decimal("3.14159"),
|
|
37
|
+
"complex_number": 3 + 4j,
|
|
38
|
+
"byte_data": b"Hello",
|
|
39
|
+
"unique_items": {1, 2, 3},
|
|
40
|
+
"custom_obj": TestCustomObject("test", 42),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Serialize
|
|
44
|
+
serialized = TypePreservationSerializer.serialize(complex_data)
|
|
45
|
+
|
|
46
|
+
# Deserialize
|
|
47
|
+
deserialized = TypePreservationSerializer.deserialize(serialized)
|
|
48
|
+
|
|
49
|
+
# Verify types
|
|
50
|
+
assert isinstance(deserialized, OrderedDict)
|
|
51
|
+
assert isinstance(deserialized["timestamp_no_tz"], datetime)
|
|
52
|
+
assert deserialized["timestamp_no_tz"] == complex_data["timestamp_no_tz"]
|
|
53
|
+
assert isinstance(deserialized["timestamp"], datetime)
|
|
54
|
+
assert deserialized["timestamp"] == complex_data["timestamp"]
|
|
55
|
+
assert isinstance(deserialized["timestamp_pst"], datetime)
|
|
56
|
+
assert deserialized["timestamp_pst"] == complex_data["timestamp_pst"]
|
|
57
|
+
assert isinstance(deserialized["today"], date)
|
|
58
|
+
assert deserialized["today"] == complex_data["today"]
|
|
59
|
+
assert isinstance(deserialized["my_time"], time)
|
|
60
|
+
assert deserialized["my_time"] == complex_data["my_time"]
|
|
61
|
+
assert isinstance(deserialized["my_time_pst"], time)
|
|
62
|
+
assert deserialized["my_time_pst"] == complex_data["my_time_pst"]
|
|
63
|
+
assert isinstance(deserialized["precise_value"], Decimal)
|
|
64
|
+
assert deserialized["precise_value"] == complex_data["precise_value"]
|
|
65
|
+
assert isinstance(deserialized["complex_number"], complex)
|
|
66
|
+
assert deserialized["complex_number"] == complex_data["complex_number"]
|
|
67
|
+
assert isinstance(deserialized["byte_data"], bytes)
|
|
68
|
+
assert deserialized["byte_data"] == complex_data["byte_data"]
|
|
69
|
+
assert isinstance(deserialized["unique_items"], set)
|
|
70
|
+
assert deserialized["unique_items"] == complex_data["unique_items"]
|
|
71
|
+
assert isinstance(deserialized["custom_obj"], TestCustomObject)
|
|
72
|
+
assert deserialized["custom_obj"] == complex_data["custom_obj"]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_nested_complex_data() -> None:
|
|
76
|
+
"""
|
|
77
|
+
Test serialization of nested complex data
|
|
78
|
+
"""
|
|
79
|
+
nested_data = {"level1": {"level2": {"timestamp": datetime.now(timezone.utc)}}}
|
|
80
|
+
|
|
81
|
+
serialized = TypePreservationSerializer.serialize(nested_data)
|
|
82
|
+
deserialized = TypePreservationSerializer.deserialize(serialized)
|
|
83
|
+
|
|
84
|
+
assert isinstance(deserialized["level1"]["level2"]["timestamp"], datetime)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_nested_dict() -> None:
|
|
88
|
+
"""
|
|
89
|
+
Test serialization of nested dictionaries
|
|
90
|
+
"""
|
|
91
|
+
logger: Logger = logging.getLogger(__name__)
|
|
92
|
+
nested_dict = {
|
|
93
|
+
"resourceType": "Coverage",
|
|
94
|
+
"id": "3456789012345670304",
|
|
95
|
+
"beneficiary": {"reference": "Patient/1234567890123456703", "type": "Patient"},
|
|
96
|
+
"class": [
|
|
97
|
+
{
|
|
98
|
+
"name": "Aetna Plan",
|
|
99
|
+
"type": {
|
|
100
|
+
"coding": [
|
|
101
|
+
{
|
|
102
|
+
"code": "plan",
|
|
103
|
+
"display": "Plan",
|
|
104
|
+
"system": "http://terminology.hl7.org/CodeSystem/coverage-class",
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"value": "AE303",
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
"costToBeneficiary": [
|
|
112
|
+
{
|
|
113
|
+
"type": {"text": "Annual Physical Exams NMC - In Network"},
|
|
114
|
+
"valueQuantity": {
|
|
115
|
+
"system": "http://aetna.com/Medicare/CostToBeneficiary/ValueQuantity/code",
|
|
116
|
+
"unit": "$",
|
|
117
|
+
"value": 50.0,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"identifier": [
|
|
122
|
+
{
|
|
123
|
+
"system": "https://sources.aetna.com/coverage/identifier/membershipid/59",
|
|
124
|
+
"type": {
|
|
125
|
+
"coding": [
|
|
126
|
+
{
|
|
127
|
+
"code": "SN",
|
|
128
|
+
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
},
|
|
132
|
+
"value": "435679010300+AE303+2021-01-01",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "uuid",
|
|
136
|
+
"system": "https://www.icanbwell.com/uuid",
|
|
137
|
+
"value": "92266603-aa8b-58c6-99bd-326fd1da1896",
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
"meta": {
|
|
141
|
+
"security": [
|
|
142
|
+
{"code": "aetna", "system": "https://www.icanbwell.com/owner"},
|
|
143
|
+
{"code": "aetna", "system": "https://www.icanbwell.com/access"},
|
|
144
|
+
{"code": "aetna", "system": "https://www.icanbwell.com/vendor"},
|
|
145
|
+
{"code": "proa", "system": "https://www.icanbwell.com/connectionType"},
|
|
146
|
+
],
|
|
147
|
+
"source": "http://mock-server:1080/test_patient_access_transformer/source/4_0_0/Coverage/3456789012345670304",
|
|
148
|
+
},
|
|
149
|
+
"network": "Medicare - MA/NY/NJ - Full Reciprocity",
|
|
150
|
+
"payor": [
|
|
151
|
+
{
|
|
152
|
+
"display": "Aetna",
|
|
153
|
+
"reference": "Organization/6667778889990000015",
|
|
154
|
+
"type": "Organization",
|
|
155
|
+
}
|
|
156
|
+
],
|
|
157
|
+
"period": {
|
|
158
|
+
"end": datetime.fromisoformat("2021-12-31").date(),
|
|
159
|
+
"start": datetime.fromisoformat("2021-01-01").date(),
|
|
160
|
+
},
|
|
161
|
+
"policyHolder": {"reference": "Patient/1234567890123456703", "type": "Patient"},
|
|
162
|
+
"relationship": {
|
|
163
|
+
"coding": [
|
|
164
|
+
{
|
|
165
|
+
"code": "self",
|
|
166
|
+
"system": "http://terminology.hl7.org/CodeSystem/subscriber-relationship",
|
|
167
|
+
}
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
"status": "active",
|
|
171
|
+
"subscriber": {"reference": "Patient/1234567890123456703", "type": "Patient"},
|
|
172
|
+
"subscriberId": "435679010300",
|
|
173
|
+
"type": {
|
|
174
|
+
"coding": [
|
|
175
|
+
{
|
|
176
|
+
"code": "PPO",
|
|
177
|
+
"display": "preferred provider organization policy",
|
|
178
|
+
"system": "http://terminology.hl7.org/CodeSystem/v3-ActCode",
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
logger.info("-------- Serialized --------")
|
|
185
|
+
serialized: str = TypePreservationSerializer.serialize(nested_dict)
|
|
186
|
+
logger.info(serialized)
|
|
187
|
+
logger.info("-------- Deserialized --------")
|
|
188
|
+
deserialized: OrderedDict[str, Any] = TypePreservationSerializer.deserialize(
|
|
189
|
+
serialized
|
|
190
|
+
)
|
|
191
|
+
logger.info(deserialized)
|
|
192
|
+
|
|
193
|
+
assert isinstance(deserialized, OrderedDict)
|
|
194
|
+
|
|
195
|
+
assert isinstance(deserialized["period"]["start"], date)
|
|
196
|
+
assert isinstance(deserialized["period"]["end"], date)
|
|
197
|
+
assert nested_dict == deserialized
|