compressedfhir 1.0.2__py3-none-any.whl → 1.0.4__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.

Files changed (29) hide show
  1. compressedfhir/fhir/fhir_bundle.py +4 -3
  2. compressedfhir/fhir/fhir_bundle_entry.py +1 -1
  3. compressedfhir/fhir/fhir_bundle_entry_response.py +0 -1
  4. compressedfhir/fhir/fhir_resource.py +37 -27
  5. compressedfhir/fhir/fhir_resource_list.py +1 -1
  6. compressedfhir/utilities/compressed_dict/v1/compressed_dict.py +50 -16
  7. compressedfhir/utilities/compressed_dict/v1/compressed_dict_storage_mode.py +7 -7
  8. compressedfhir/utilities/compressed_dict/v1/test/test_compressed_dict.py +107 -0
  9. compressedfhir/utilities/fhir_json_encoder.py +1 -1
  10. compressedfhir/utilities/json_serializers/__init__.py +0 -0
  11. compressedfhir/utilities/json_serializers/test/__init__.py +0 -0
  12. compressedfhir/utilities/json_serializers/test/test_type_preservation_decoder.py +150 -0
  13. compressedfhir/utilities/json_serializers/test/test_type_preservation_encoder.py +59 -0
  14. compressedfhir/utilities/json_serializers/test/test_type_preservation_serializer.py +171 -0
  15. compressedfhir/utilities/json_serializers/type_preservation_decoder.py +110 -0
  16. compressedfhir/utilities/json_serializers/type_preservation_encoder.py +50 -0
  17. compressedfhir/utilities/json_serializers/type_preservation_serializer.py +57 -0
  18. compressedfhir/utilities/ordered_dict_to_dict_converter/__init__.py +0 -0
  19. compressedfhir/utilities/ordered_dict_to_dict_converter/ordered_dict_to_dict_converter.py +24 -0
  20. compressedfhir/utilities/string_compressor/__init__.py +0 -0
  21. compressedfhir/utilities/string_compressor/v1/__init__.py +0 -0
  22. compressedfhir/utilities/string_compressor/v1/string_compressor.py +99 -0
  23. compressedfhir/utilities/string_compressor/v1/test/__init__.py +0 -0
  24. compressedfhir/utilities/string_compressor/v1/test/test_string_compressor.py +189 -0
  25. {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/METADATA +1 -1
  26. {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/RECORD +29 -14
  27. {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/WHEEL +0 -0
  28. {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/licenses/LICENSE +0 -0
  29. {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,171 @@
1
+ import logging
2
+ from datetime import datetime, timezone, date
3
+ from decimal import Decimal
4
+ from logging import Logger
5
+ from typing import Any
6
+
7
+ from compressedfhir.utilities.json_serializers.type_preservation_serializer import (
8
+ TypePreservationSerializer,
9
+ )
10
+
11
+
12
+ class TestCustomObject:
13
+ def __init__(self, name: str, value: int):
14
+ self.name: str = name
15
+ self.value: int = value
16
+
17
+ def __eq__(self, other: Any) -> bool:
18
+ if not isinstance(other, TestCustomObject):
19
+ return False
20
+ return self.name == other.name and self.value == other.value
21
+
22
+
23
+ def test_complex_data_serialization() -> None:
24
+ """
25
+ Test serialization and deserialization of complex data
26
+ """
27
+ complex_data = {
28
+ "timestamp": datetime.now(timezone.utc),
29
+ "today": date.today(),
30
+ "precise_value": Decimal("3.14159"),
31
+ "complex_number": 3 + 4j,
32
+ "byte_data": b"Hello",
33
+ "unique_items": {1, 2, 3},
34
+ "custom_obj": TestCustomObject("test", 42),
35
+ }
36
+
37
+ # Serialize
38
+ serialized = TypePreservationSerializer.serialize(complex_data)
39
+
40
+ # Deserialize
41
+ deserialized = TypePreservationSerializer.deserialize(serialized)
42
+
43
+ # Verify types
44
+ assert isinstance(deserialized["timestamp"], datetime)
45
+ assert isinstance(deserialized["today"], date)
46
+ assert isinstance(deserialized["precise_value"], Decimal)
47
+ assert isinstance(deserialized["complex_number"], complex)
48
+ assert isinstance(deserialized["byte_data"], bytes)
49
+ assert isinstance(deserialized["unique_items"], set)
50
+ assert isinstance(deserialized["custom_obj"], TestCustomObject)
51
+
52
+
53
+ def test_nested_complex_data() -> None:
54
+ """
55
+ Test serialization of nested complex data
56
+ """
57
+ nested_data = {"level1": {"level2": {"timestamp": datetime.now(timezone.utc)}}}
58
+
59
+ serialized = TypePreservationSerializer.serialize(nested_data)
60
+ deserialized = TypePreservationSerializer.deserialize(serialized)
61
+
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
@@ -0,0 +1,110 @@
1
+ import logging
2
+ from collections import OrderedDict
3
+ from datetime import datetime, date
4
+ from decimal import Decimal
5
+ from logging import Logger
6
+ from typing import Any, Dict, Callable, Optional, Union, cast, List
7
+
8
+
9
+ class TypePreservationDecoder:
10
+ """
11
+ Advanced JSON decoder for complex type reconstruction with nested type support
12
+ """
13
+
14
+ @classmethod
15
+ def decode(
16
+ cls,
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,
20
+ ) -> Any:
21
+ """
22
+ Decode complex types, including nested datetime fields
23
+
24
+ Args:
25
+ dct: Dictionary to decode
26
+ custom_decoders: Optional additional custom decoders
27
+ use_ordered_dict: Flag to control whether to use OrderedDict or not
28
+
29
+ Returns:
30
+ Reconstructed object or original dictionary
31
+ """
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
+
49
+ default_decoders: Dict[str, Callable[[Any], Any]] = {
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,
60
+ }
61
+
62
+ # Merge custom decoders with default decoders
63
+ decoders = {**default_decoders, **(custom_decoders or {})}
64
+
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)
@@ -0,0 +1,50 @@
1
+ import json
2
+ from collections.abc import Callable
3
+ from datetime import datetime, date
4
+ from decimal import Decimal
5
+ from typing import Any, Dict, Type
6
+
7
+
8
+ class TypePreservationEncoder(json.JSONEncoder):
9
+ """
10
+ Advanced JSON encoder for complex type serialization
11
+ """
12
+
13
+ TYPE_MAP: Dict[Type[Any], Callable[[Any], Any]] = {
14
+ datetime: lambda dt: {
15
+ "__type__": "datetime",
16
+ "iso": dt.isoformat(),
17
+ "tzinfo": str(dt.tzinfo) if dt.tzinfo else None,
18
+ },
19
+ date: lambda d: {"__type__": "date", "iso": d.isoformat()},
20
+ Decimal: lambda d: {"__type__": "decimal", "value": str(d)},
21
+ complex: lambda c: {"__type__": "complex", "real": c.real, "imag": c.imag},
22
+ bytes: lambda b: {"__type__": "bytes", "value": b.decode("latin-1")},
23
+ set: lambda s: {"__type__": "set", "values": list(s)},
24
+ }
25
+
26
+ def default(self, obj: Any) -> Any:
27
+ """
28
+ Custom serialization for complex types
29
+
30
+ Args:
31
+ obj: Object to serialize
32
+
33
+ Returns:
34
+ Serializable representation of the object
35
+ """
36
+ # Check if the type is in our custom type map
37
+ for type_, serializer in self.TYPE_MAP.items():
38
+ if isinstance(obj, type_):
39
+ return serializer(obj)
40
+
41
+ # Handle custom objects with __dict__
42
+ if hasattr(obj, "__dict__"):
43
+ return {
44
+ "__type__": obj.__class__.__name__,
45
+ "__module__": obj.__class__.__module__,
46
+ "attributes": obj.__dict__,
47
+ }
48
+
49
+ # Fallback to default JSON encoder
50
+ return super().default(obj)
@@ -0,0 +1,57 @@
1
+ import json
2
+ from typing import Any, Callable, Dict
3
+
4
+ from compressedfhir.utilities.json_serializers.type_preservation_decoder import (
5
+ TypePreservationDecoder,
6
+ )
7
+ from compressedfhir.utilities.json_serializers.type_preservation_encoder import (
8
+ TypePreservationEncoder,
9
+ )
10
+
11
+
12
+ class TypePreservationSerializer:
13
+ """
14
+ Comprehensive serialization and deserialization utility
15
+ """
16
+
17
+ @classmethod
18
+ def serialize(cls, data: Any, **kwargs: Any) -> str:
19
+ """
20
+ Serialize data with advanced type handling
21
+
22
+ Args:
23
+ data: Data to serialize
24
+ kwargs: Additional JSON dumps arguments
25
+
26
+ Returns:
27
+ JSON string representation
28
+ """
29
+ return json.dumps(
30
+ data, cls=TypePreservationEncoder, separators=(",", ":"), **kwargs
31
+ )
32
+
33
+ @classmethod
34
+ def deserialize(
35
+ cls,
36
+ json_str: str,
37
+ custom_decoders: Dict[str, Callable[[Any], Any]] | None = None,
38
+ **kwargs: Any,
39
+ ) -> Any:
40
+ """
41
+ Deserialize JSON string with advanced type reconstruction
42
+
43
+ Args:
44
+ json_str: JSON string to deserialize
45
+ custom_decoders: Optional additional custom decoders
46
+ kwargs: Additional JSON loads arguments
47
+
48
+ Returns:
49
+ Reconstructed object
50
+ """
51
+ return json.loads(
52
+ json_str,
53
+ object_hook=lambda dct: TypePreservationDecoder.decode(
54
+ dct, custom_decoders
55
+ ),
56
+ **kwargs,
57
+ )
@@ -0,0 +1,24 @@
1
+ from typing import OrderedDict, cast
2
+
3
+
4
+ class OrderedDictToDictConverter:
5
+ @staticmethod
6
+ def convert[K, V](ordered_dict: OrderedDict[K, V]) -> dict[K, V]:
7
+ """
8
+ Converts an OrderedDict to a regular dict in a recursive manner.
9
+
10
+
11
+ :param ordered_dict: The OrderedDict to convert
12
+ :return: A regular dict with the same key-value pairs
13
+ """
14
+
15
+ def _convert[T](value: T) -> T:
16
+ if isinstance(value, OrderedDict):
17
+ return cast(T, {k: _convert(v) for k, v in value.items()})
18
+ elif isinstance(value, dict):
19
+ return cast(T, {k: _convert(v) for k, v in value.items()})
20
+ elif isinstance(value, list):
21
+ return cast(T, [_convert(item) for item in value])
22
+ return value
23
+
24
+ return _convert(ordered_dict)
File without changes
@@ -0,0 +1,99 @@
1
+ import zlib
2
+ from typing import Union, Optional
3
+
4
+
5
+ class StringCompressor:
6
+ """
7
+ A utility class for compressing and decompressing strings using zlib.
8
+
9
+ Provides methods to compress strings to bytes and decompress bytes back to strings.
10
+ Uses UTF-8 encoding and zlib's best compression level.
11
+ """
12
+
13
+ @staticmethod
14
+ def compress(text: str, encoding: str = "utf-8") -> bytes:
15
+ """
16
+ Compress a given string to bytes using zlib.
17
+
18
+ Args:
19
+ text (str): The input string to compress
20
+ encoding (str, optional): The encoding to use. Defaults to 'utf-8'
21
+
22
+ Returns:
23
+ bytes: Compressed representation of the input string
24
+
25
+ Raises:
26
+ TypeError: If input is not a string
27
+ zlib.error: If compression fails
28
+ """
29
+ if not isinstance(text, str):
30
+ raise TypeError("Input must be a string")
31
+
32
+ try:
33
+ # Encode string to bytes, then compress with best compression
34
+ return zlib.compress(text.encode(encoding), level=zlib.Z_BEST_COMPRESSION)
35
+ except Exception as e:
36
+ raise zlib.error(f"Compression failed: {e}")
37
+
38
+ @staticmethod
39
+ def decompress(
40
+ compressed_data: Union[bytes, bytearray], encoding: str = "utf-8"
41
+ ) -> str:
42
+ """
43
+ Decompress bytes back to the original string.
44
+
45
+ Args:
46
+ compressed_data (Union[bytes, bytearray]): Compressed data to decompress
47
+ encoding (str, optional): The encoding to use. Defaults to 'utf-8'
48
+
49
+ Returns:
50
+ str: Decompressed original string
51
+
52
+ Raises:
53
+ TypeError: If input is not bytes or bytearray
54
+ zlib.error: If decompression fails
55
+ """
56
+ if not isinstance(compressed_data, (bytes, bytearray)):
57
+ raise TypeError("Input must be bytes or bytearray")
58
+
59
+ try:
60
+ # Decompress bytes, then decode to string
61
+ return zlib.decompress(compressed_data).decode(encoding)
62
+ except Exception as e:
63
+ raise zlib.error(f"Decompression failed: {e}")
64
+
65
+ @classmethod
66
+ def compress_safe(
67
+ cls, text: Optional[str], encoding: str = "utf-8"
68
+ ) -> Optional[bytes]:
69
+ """
70
+ Safely compress a string, handling None input.
71
+
72
+ Args:
73
+ text (Optional[str]): The input string to compress
74
+ encoding (str, optional): The encoding to use. Defaults to 'utf-8'
75
+
76
+ Returns:
77
+ Optional[bytes]: Compressed bytes or None if input is None
78
+ """
79
+ if text is None:
80
+ return None
81
+ return cls.compress(text, encoding)
82
+
83
+ @classmethod
84
+ def decompress_safe(
85
+ cls, compressed_data: Optional[Union[bytes, bytearray]], encoding: str = "utf-8"
86
+ ) -> Optional[str]:
87
+ """
88
+ Safely decompress bytes, handling None input.
89
+
90
+ Args:
91
+ compressed_data (Optional[Union[bytes, bytearray]]): Compressed data to decompress
92
+ encoding (str, optional): The encoding to use. Defaults to 'utf-8'
93
+
94
+ Returns:
95
+ Optional[str]: Decompressed string or None if input is None
96
+ """
97
+ if compressed_data is None:
98
+ return None
99
+ return cls.decompress(compressed_data, encoding)