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.
- compressedfhir/fhir/fhir_bundle.py +4 -3
- compressedfhir/fhir/fhir_bundle_entry.py +1 -1
- compressedfhir/fhir/fhir_bundle_entry_response.py +0 -1
- compressedfhir/fhir/fhir_resource.py +37 -27
- compressedfhir/fhir/fhir_resource_list.py +1 -1
- compressedfhir/utilities/compressed_dict/v1/compressed_dict.py +50 -16
- compressedfhir/utilities/compressed_dict/v1/compressed_dict_storage_mode.py +7 -7
- compressedfhir/utilities/compressed_dict/v1/test/test_compressed_dict.py +107 -0
- compressedfhir/utilities/fhir_json_encoder.py +1 -1
- 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 +150 -0
- compressedfhir/utilities/json_serializers/test/test_type_preservation_encoder.py +59 -0
- compressedfhir/utilities/json_serializers/test/test_type_preservation_serializer.py +171 -0
- compressedfhir/utilities/json_serializers/type_preservation_decoder.py +110 -0
- compressedfhir/utilities/json_serializers/type_preservation_encoder.py +50 -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-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/METADATA +1 -1
- {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/RECORD +29 -14
- {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/WHEEL +0 -0
- {compressedfhir-1.0.2.dist-info → compressedfhir-1.0.4.dist-info}/licenses/LICENSE +0 -0
- {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
|
+
)
|
|
File without changes
|
|
@@ -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
|
|
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)
|
|
File without changes
|