polyxml 0.5.0__tar.gz → 0.6.0__tar.gz
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.
- {polyxml-0.5.0 → polyxml-0.6.0}/Cargo.lock +4 -4
- {polyxml-0.5.0 → polyxml-0.6.0}/Cargo.toml +1 -1
- {polyxml-0.5.0 → polyxml-0.6.0}/PKG-INFO +31 -2
- polyxml-0.6.0/README.md +34 -0
- polyxml-0.6.0/crates/polyxml-python/README.md +34 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-python/tests/test_polyxml.py +140 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/pyproject.toml +1 -1
- {polyxml-0.5.0 → polyxml-0.6.0}/python/polyxml/__init__.py +93 -6
- polyxml-0.5.0/README.md +0 -5
- polyxml-0.5.0/crates/polyxml-python/README.md +0 -5
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/Cargo.toml +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/README.md +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/converters.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/error.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/lib.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/parser.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/schema.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/serializer.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/src/value.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/tests/test_core.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-core/tests/test_security.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-python/Cargo.toml +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/crates/polyxml-python/src/lib.rs +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/python/polyxml/AGENT_GUIDE.md +0 -0
- {polyxml-0.5.0 → polyxml-0.6.0}/python/polyxml/py.typed +0 -0
|
@@ -395,7 +395,7 @@ dependencies = [
|
|
|
395
395
|
|
|
396
396
|
[[package]]
|
|
397
397
|
name = "polyxml"
|
|
398
|
-
version = "0.
|
|
398
|
+
version = "0.6.0"
|
|
399
399
|
dependencies = [
|
|
400
400
|
"criterion",
|
|
401
401
|
"lexical-core",
|
|
@@ -408,14 +408,14 @@ dependencies = [
|
|
|
408
408
|
|
|
409
409
|
[[package]]
|
|
410
410
|
name = "polyxml-c"
|
|
411
|
-
version = "0.
|
|
411
|
+
version = "0.6.0"
|
|
412
412
|
dependencies = [
|
|
413
413
|
"polyxml",
|
|
414
414
|
]
|
|
415
415
|
|
|
416
416
|
[[package]]
|
|
417
417
|
name = "polyxml-js"
|
|
418
|
-
version = "0.
|
|
418
|
+
version = "0.6.0"
|
|
419
419
|
dependencies = [
|
|
420
420
|
"napi",
|
|
421
421
|
"napi-build",
|
|
@@ -425,7 +425,7 @@ dependencies = [
|
|
|
425
425
|
|
|
426
426
|
[[package]]
|
|
427
427
|
name = "polyxml-python"
|
|
428
|
-
version = "0.
|
|
428
|
+
version = "0.6.0"
|
|
429
429
|
dependencies = [
|
|
430
430
|
"lexical-core",
|
|
431
431
|
"polyxml",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polyxml
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -34,5 +34,34 @@ Project-URL: Repository, https://github.com/nth-bailey/PolyXML
|
|
|
34
34
|
|
|
35
35
|
High-performance native XML data-binding engine for Python dataclasses and Pydantic models.
|
|
36
36
|
|
|
37
|
-
Powered by polyxml-core written in Rust and PyO3 (abi3-py312).
|
|
37
|
+
Powered by `polyxml-core` written in Rust and PyO3 (`abi3-py312`).
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **⚡ Blazing Fast**: 16x faster deserialization and 38x faster serialization than standard Python tools.
|
|
42
|
+
- **🌊 Streaming `iterparse()`**: Parse multi-gigabyte XML files with O(1) constant memory (<5 MB RAM).
|
|
43
|
+
- **📦 Zero-GIL Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`), up to 12.4x faster than pickle.
|
|
44
|
+
- **🎯 Full Type Support**: Dataclasses and Pydantic v2 models with zero boilerplate.
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from dataclasses import dataclass, field
|
|
50
|
+
import polyxml
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Item:
|
|
54
|
+
id: int = field(metadata={"type": "Attribute"})
|
|
55
|
+
name: str = field(metadata={"type": "Element"})
|
|
56
|
+
|
|
57
|
+
# 1. XML Deserialization
|
|
58
|
+
item = polyxml.deserialize(b'<Item id="1"><name>Gadget</name></Item>', Item)
|
|
59
|
+
|
|
60
|
+
# 2. XML Serialization
|
|
61
|
+
xml = polyxml.serialize(item, indent=2)
|
|
62
|
+
|
|
63
|
+
# 3. High-Speed Binary Serialization (Key-Value databases / IPC)
|
|
64
|
+
blob = polyxml.dumps_binary(item)
|
|
65
|
+
restored = polyxml.loads_binary(blob, Item)
|
|
66
|
+
```
|
|
38
67
|
|
polyxml-0.6.0/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PolyXML Python Bindings
|
|
2
|
+
|
|
3
|
+
High-performance native XML data-binding engine for Python dataclasses and Pydantic models.
|
|
4
|
+
|
|
5
|
+
Powered by `polyxml-core` written in Rust and PyO3 (`abi3-py312`).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **⚡ Blazing Fast**: 16x faster deserialization and 38x faster serialization than standard Python tools.
|
|
10
|
+
- **🌊 Streaming `iterparse()`**: Parse multi-gigabyte XML files with O(1) constant memory (<5 MB RAM).
|
|
11
|
+
- **📦 Zero-GIL Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`), up to 12.4x faster than pickle.
|
|
12
|
+
- **🎯 Full Type Support**: Dataclasses and Pydantic v2 models with zero boilerplate.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
import polyxml
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Item:
|
|
22
|
+
id: int = field(metadata={"type": "Attribute"})
|
|
23
|
+
name: str = field(metadata={"type": "Element"})
|
|
24
|
+
|
|
25
|
+
# 1. XML Deserialization
|
|
26
|
+
item = polyxml.deserialize(b'<Item id="1"><name>Gadget</name></Item>', Item)
|
|
27
|
+
|
|
28
|
+
# 2. XML Serialization
|
|
29
|
+
xml = polyxml.serialize(item, indent=2)
|
|
30
|
+
|
|
31
|
+
# 3. High-Speed Binary Serialization (Key-Value databases / IPC)
|
|
32
|
+
blob = polyxml.dumps_binary(item)
|
|
33
|
+
restored = polyxml.loads_binary(blob, Item)
|
|
34
|
+
```
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# PolyXML Python Bindings
|
|
2
|
+
|
|
3
|
+
High-performance native XML data-binding engine for Python dataclasses and Pydantic models.
|
|
4
|
+
|
|
5
|
+
Powered by `polyxml-core` written in Rust and PyO3 (`abi3-py312`).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **⚡ Blazing Fast**: 16x faster deserialization and 38x faster serialization than standard Python tools.
|
|
10
|
+
- **🌊 Streaming `iterparse()`**: Parse multi-gigabyte XML files with O(1) constant memory (<5 MB RAM).
|
|
11
|
+
- **📦 Zero-GIL Binary Serialization**: Native `dumps_binary()` and `loads_binary()` using MessagePack (`msgspec`), up to 12.4x faster than pickle.
|
|
12
|
+
- **🎯 Full Type Support**: Dataclasses and Pydantic v2 models with zero boilerplate.
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
import polyxml
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Item:
|
|
22
|
+
id: int = field(metadata={"type": "Attribute"})
|
|
23
|
+
name: str = field(metadata={"type": "Element"})
|
|
24
|
+
|
|
25
|
+
# 1. XML Deserialization
|
|
26
|
+
item = polyxml.deserialize(b'<Item id="1"><name>Gadget</name></Item>', Item)
|
|
27
|
+
|
|
28
|
+
# 2. XML Serialization
|
|
29
|
+
xml = polyxml.serialize(item, indent=2)
|
|
30
|
+
|
|
31
|
+
# 3. High-Speed Binary Serialization (Key-Value databases / IPC)
|
|
32
|
+
blob = polyxml.dumps_binary(item)
|
|
33
|
+
restored = polyxml.loads_binary(blob, Item)
|
|
34
|
+
```
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import io
|
|
2
2
|
import pathlib
|
|
3
|
+
from collections import UserString
|
|
3
4
|
from dataclasses import dataclass, field
|
|
4
5
|
from decimal import Decimal
|
|
5
6
|
from enum import Enum
|
|
7
|
+
from xml.etree.ElementTree import QName
|
|
6
8
|
|
|
9
|
+
import msgspec
|
|
7
10
|
import pytest
|
|
8
11
|
from pydantic import BaseModel, Field
|
|
9
12
|
|
|
@@ -402,3 +405,140 @@ def test_binary_deserialization_unsupported_type():
|
|
|
402
405
|
payload = polyxml.dumps_binary({"a": 1})
|
|
403
406
|
with pytest.raises(NotImplementedError, match="PolyXML binary deserializer cannot deserialize"):
|
|
404
407
|
polyxml.loads_binary(payload, UnhandledClass)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class StatusEnum(Enum):
|
|
411
|
+
ACTIVE = "active"
|
|
412
|
+
INACTIVE = "inactive"
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class CustomDuration(UserString):
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class FromStringHolder:
|
|
420
|
+
def __init__(self, val: str):
|
|
421
|
+
self.val = val
|
|
422
|
+
|
|
423
|
+
@classmethod
|
|
424
|
+
def from_string(cls, val: str):
|
|
425
|
+
return cls(val)
|
|
426
|
+
|
|
427
|
+
def __str__(self):
|
|
428
|
+
return self.val
|
|
429
|
+
|
|
430
|
+
def __eq__(self, other):
|
|
431
|
+
return isinstance(other, FromStringHolder) and self.val == other.val
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@dataclass
|
|
435
|
+
class RichXmlItem:
|
|
436
|
+
status: StatusEnum
|
|
437
|
+
qname: QName
|
|
438
|
+
duration: CustomDuration
|
|
439
|
+
holder: FromStringHolder
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def test_binary_serialization_xml_types():
|
|
443
|
+
orig = RichXmlItem(
|
|
444
|
+
status=StatusEnum.ACTIVE,
|
|
445
|
+
qname=QName("http://example.com", "elem"),
|
|
446
|
+
duration=CustomDuration("P1Y2M3D"),
|
|
447
|
+
holder=FromStringHolder("parsed_value"),
|
|
448
|
+
)
|
|
449
|
+
payload = polyxml.dumps_binary(orig)
|
|
450
|
+
decoded = polyxml.loads_binary(payload, RichXmlItem)
|
|
451
|
+
assert decoded.status == StatusEnum.ACTIVE
|
|
452
|
+
assert decoded.qname == QName("http://example.com", "elem")
|
|
453
|
+
assert decoded.duration == CustomDuration("P1Y2M3D")
|
|
454
|
+
assert decoded.holder == FromStringHolder("parsed_value")
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def test_binary_serialization_tag_class_dataclass():
|
|
458
|
+
item = SimpleItem(id=99, name="TaggedItem", price=49.99, active=True)
|
|
459
|
+
payload = polyxml.dumps_binary(item, tag_class=True)
|
|
460
|
+
|
|
461
|
+
# Loads without passing target_type
|
|
462
|
+
decoded_auto = polyxml.loads_binary(payload)
|
|
463
|
+
assert isinstance(decoded_auto, SimpleItem)
|
|
464
|
+
assert decoded_auto.id == 99
|
|
465
|
+
assert decoded_auto.name == "TaggedItem"
|
|
466
|
+
|
|
467
|
+
# Loads with explicit target_type on tagged payload
|
|
468
|
+
decoded_explicit = polyxml.loads_binary(payload, SimpleItem)
|
|
469
|
+
assert isinstance(decoded_explicit, SimpleItem)
|
|
470
|
+
assert decoded_explicit.id == 99
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_binary_serialization_tag_class_pydantic():
|
|
474
|
+
device = PydanticDevice(serial="SN-9999", model="AeroEdge", power=250.0)
|
|
475
|
+
payload = polyxml.dumps_binary(device, tag_class=True)
|
|
476
|
+
decoded = polyxml.loads_binary(payload)
|
|
477
|
+
assert isinstance(decoded, PydanticDevice)
|
|
478
|
+
assert decoded.serial == "SN-9999"
|
|
479
|
+
assert decoded.model == "AeroEdge"
|
|
480
|
+
assert decoded.power == 250.0
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def test_binary_serialization_tag_class_primitive():
|
|
484
|
+
payload = polyxml.dumps_binary("plain_string", tag_class=True)
|
|
485
|
+
decoded = polyxml.loads_binary(payload)
|
|
486
|
+
assert decoded == "plain_string"
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_binary_serialization_hooks_direct():
|
|
490
|
+
from polyxml import _dec_hook, _enc_hook
|
|
491
|
+
|
|
492
|
+
assert _enc_hook(StatusEnum.ACTIVE) == "active"
|
|
493
|
+
assert _dec_hook(Decimal, "19.99") == Decimal("19.99")
|
|
494
|
+
assert _dec_hook(StatusEnum, "active") == StatusEnum.ACTIVE
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def test_binary_serialization_resolve_class():
|
|
498
|
+
from polyxml import _resolve_class
|
|
499
|
+
|
|
500
|
+
assert _resolve_class("") is None
|
|
501
|
+
assert _resolve_class("NoColonHere") is None
|
|
502
|
+
assert _resolve_class("nonexistent.module:FakeClass") is None
|
|
503
|
+
assert _resolve_class("polyxml:dumps_binary") is polyxml.dumps_binary
|
|
504
|
+
# Hits the _CLASS_CACHE branch
|
|
505
|
+
assert _resolve_class("polyxml:dumps_binary") is polyxml.dumps_binary
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def test_binary_serialization_tagged_unresolvable_class():
|
|
509
|
+
# Tagged payload with a class that cannot be resolved
|
|
510
|
+
raw_inner = msgspec.msgpack.encode({"key": "value"})
|
|
511
|
+
tagged_data = msgspec.msgpack.encode(("nonexistent.module:FakeClass", raw_inner))
|
|
512
|
+
|
|
513
|
+
decoded = polyxml.loads_binary(tagged_data)
|
|
514
|
+
assert decoded == {"key": "value"}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def test_binary_serialization_tagged_payload_decode_error():
|
|
518
|
+
# Explicit target_type on tagged payload where inner payload fails to decode
|
|
519
|
+
raw_inner = msgspec.msgpack.encode({"unexpected": "structure"})
|
|
520
|
+
tagged_data = msgspec.msgpack.encode(("nonexistent:Fake", raw_inner))
|
|
521
|
+
|
|
522
|
+
with pytest.raises(msgspec.ValidationError):
|
|
523
|
+
polyxml.loads_binary(tagged_data, SimpleItem)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_binary_serialization_target_type_non_tagged_decode_errors():
|
|
527
|
+
# Target type passed with invalid msgpack bytes
|
|
528
|
+
with pytest.raises(msgspec.DecodeError):
|
|
529
|
+
polyxml.loads_binary(b"\xc1", SimpleItem)
|
|
530
|
+
|
|
531
|
+
# Target type passed with valid non-tagged msgpack that fails validation
|
|
532
|
+
with pytest.raises(msgspec.ValidationError):
|
|
533
|
+
polyxml.loads_binary(msgspec.msgpack.encode(123), SimpleItem)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def test_binary_serialization_loads_corrupted_or_non_tagged():
|
|
537
|
+
# Plain non-tagged list of two items where second is not bytes
|
|
538
|
+
data = msgspec.msgpack.encode(["not_a_class_tag", 12345])
|
|
539
|
+
decoded = polyxml.loads_binary(data)
|
|
540
|
+
assert decoded == ["not_a_class_tag", 12345]
|
|
541
|
+
|
|
542
|
+
# Non-decodable data to custom type
|
|
543
|
+
with pytest.raises(msgspec.DecodeError):
|
|
544
|
+
polyxml.loads_binary(b"\xc1")
|
|
@@ -24,10 +24,15 @@ QUICKSTART FOR AI AGENTS & DEVELOPERS:
|
|
|
24
24
|
See `AGENT_GUIDE.md` or https://nth-bailey.github.io/PolyXML/ for complete documentation.
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
|
+
import importlib
|
|
27
28
|
import pathlib
|
|
29
|
+
from collections import UserString
|
|
28
30
|
from collections.abc import Iterator
|
|
31
|
+
from dataclasses import is_dataclass
|
|
29
32
|
from decimal import Decimal
|
|
33
|
+
from enum import Enum
|
|
30
34
|
from typing import IO
|
|
35
|
+
from xml.etree.ElementTree import QName
|
|
31
36
|
|
|
32
37
|
from polyxml._polyxml import ( # type: ignore[import-not-found]
|
|
33
38
|
deserialize as _deserialize,
|
|
@@ -120,11 +125,38 @@ except ImportError: # pragma: no cover
|
|
|
120
125
|
msgspec = None # type: ignore[assignment]
|
|
121
126
|
_HAS_MSGSPEC = False
|
|
122
127
|
|
|
128
|
+
_CLASS_CACHE: dict[str, type[object]] = {}
|
|
123
129
|
|
|
124
|
-
|
|
130
|
+
|
|
131
|
+
def _resolve_class(class_identifier: str) -> type[object] | None:
|
|
132
|
+
if not class_identifier:
|
|
133
|
+
return None
|
|
134
|
+
if class_identifier in _CLASS_CACHE:
|
|
135
|
+
return _CLASS_CACHE[class_identifier]
|
|
136
|
+
if ":" in class_identifier:
|
|
137
|
+
module_name, class_name = class_identifier.rsplit(":", 1)
|
|
138
|
+
try:
|
|
139
|
+
mod = importlib.import_module(module_name)
|
|
140
|
+
resolved = getattr(mod, class_name)
|
|
141
|
+
_CLASS_CACHE[class_identifier] = resolved
|
|
142
|
+
return resolved
|
|
143
|
+
except (ImportError, AttributeError):
|
|
144
|
+
return None
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _enc_hook(obj: object) -> object:
|
|
125
149
|
if isinstance(obj, pathlib.Path | Decimal):
|
|
126
150
|
return str(obj)
|
|
127
|
-
if
|
|
151
|
+
if isinstance(obj, Enum):
|
|
152
|
+
return obj.value
|
|
153
|
+
if isinstance(obj, QName):
|
|
154
|
+
return str(obj)
|
|
155
|
+
if isinstance(obj, UserString):
|
|
156
|
+
return str(obj)
|
|
157
|
+
if hasattr(obj, "model_dump") and callable(obj.model_dump):
|
|
158
|
+
return obj.model_dump()
|
|
159
|
+
if hasattr(obj, "from_string"):
|
|
128
160
|
return str(obj)
|
|
129
161
|
raise NotImplementedError(
|
|
130
162
|
f"PolyXML binary serializer cannot serialize object of type {type(obj).__name__}"
|
|
@@ -134,18 +166,30 @@ def _enc_hook(obj: object) -> str:
|
|
|
134
166
|
def _dec_hook(target_type: type[object], obj: object) -> object:
|
|
135
167
|
if target_type is pathlib.Path:
|
|
136
168
|
return pathlib.Path(str(obj))
|
|
169
|
+
if target_type is Decimal:
|
|
170
|
+
return Decimal(str(obj))
|
|
171
|
+
if isinstance(target_type, type) and issubclass(target_type, Enum):
|
|
172
|
+
return target_type(obj)
|
|
173
|
+
if target_type is QName or (isinstance(target_type, type) and issubclass(target_type, QName)):
|
|
174
|
+
return QName(str(obj))
|
|
175
|
+
if hasattr(target_type, "model_validate") and callable(target_type.model_validate):
|
|
176
|
+
return target_type.model_validate(obj)
|
|
137
177
|
if hasattr(target_type, "from_string"):
|
|
138
178
|
return target_type.from_string(str(obj))
|
|
179
|
+
if isinstance(target_type, type) and issubclass(target_type, UserString):
|
|
180
|
+
return target_type(str(obj))
|
|
139
181
|
raise NotImplementedError(
|
|
140
182
|
f"PolyXML binary deserializer cannot deserialize object into type {target_type.__name__}"
|
|
141
183
|
)
|
|
142
184
|
|
|
143
185
|
|
|
144
|
-
def dumps_binary(obj: object) -> bytes:
|
|
186
|
+
def dumps_binary(obj: object, *, tag_class: bool = False) -> bytes:
|
|
145
187
|
"""Serialize a model or dataclass into a high-throughput binary MessagePack buffer.
|
|
146
188
|
|
|
147
189
|
Args:
|
|
148
190
|
obj: Python dataclass, Pydantic model, or object.
|
|
191
|
+
tag_class: When True, embeds the class identifier in the binary payload
|
|
192
|
+
allowing loads_binary to reconstruct the exact model class dynamically.
|
|
149
193
|
|
|
150
194
|
Returns:
|
|
151
195
|
Compact binary MessagePack bytes.
|
|
@@ -158,6 +202,16 @@ def dumps_binary(obj: object) -> bytes:
|
|
|
158
202
|
"PolyXML binary serialization requires 'msgspec'. "
|
|
159
203
|
"Install it via: pip install 'polyxml[msgpack]' or pip install msgspec"
|
|
160
204
|
)
|
|
205
|
+
if tag_class:
|
|
206
|
+
if is_dataclass(obj) or hasattr(obj.__class__, "model_fields"):
|
|
207
|
+
cls = obj.__class__
|
|
208
|
+
identifier = f"{cls.__module__}:{cls.__qualname__}"
|
|
209
|
+
_CLASS_CACHE[identifier] = cls
|
|
210
|
+
payload = msgspec.msgpack.encode(obj, enc_hook=_enc_hook)
|
|
211
|
+
return msgspec.msgpack.encode((identifier, payload))
|
|
212
|
+
payload = msgspec.msgpack.encode(obj, enc_hook=_enc_hook)
|
|
213
|
+
return msgspec.msgpack.encode(("", payload))
|
|
214
|
+
|
|
161
215
|
return msgspec.msgpack.encode(obj, enc_hook=_enc_hook)
|
|
162
216
|
|
|
163
217
|
|
|
@@ -166,7 +220,9 @@ def loads_binary[T](data: bytes, target_type: type[T] | None = None) -> T | obje
|
|
|
166
220
|
|
|
167
221
|
Args:
|
|
168
222
|
data: Binary MessagePack bytes to decode.
|
|
169
|
-
target_type: Optional target dataclass or model class. If omitted
|
|
223
|
+
target_type: Optional target dataclass or model class. If omitted and the
|
|
224
|
+
payload was serialized with tag_class=True, dynamically reconstructs the
|
|
225
|
+
original model instance. Otherwise returns a dynamic dictionary/structure.
|
|
170
226
|
|
|
171
227
|
Returns:
|
|
172
228
|
The deserialized model instance or object.
|
|
@@ -180,8 +236,39 @@ def loads_binary[T](data: bytes, target_type: type[T] | None = None) -> T | obje
|
|
|
180
236
|
"Install it via: pip install 'polyxml[msgpack]' or pip install msgspec"
|
|
181
237
|
)
|
|
182
238
|
if target_type is not None:
|
|
183
|
-
|
|
184
|
-
|
|
239
|
+
try:
|
|
240
|
+
return msgspec.msgpack.decode(data, type=target_type, dec_hook=_dec_hook)
|
|
241
|
+
except (msgspec.DecodeError, msgspec.ValidationError):
|
|
242
|
+
try:
|
|
243
|
+
decoded = msgspec.msgpack.decode(data)
|
|
244
|
+
if (
|
|
245
|
+
isinstance(decoded, (list, tuple))
|
|
246
|
+
and len(decoded) == 2
|
|
247
|
+
and isinstance(decoded[0], str)
|
|
248
|
+
and isinstance(decoded[1], (bytes, bytearray))
|
|
249
|
+
):
|
|
250
|
+
return msgspec.msgpack.decode(decoded[1], type=target_type, dec_hook=_dec_hook)
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
raise
|
|
254
|
+
|
|
255
|
+
try:
|
|
256
|
+
decoded = msgspec.msgpack.decode(data)
|
|
257
|
+
if (
|
|
258
|
+
isinstance(decoded, (list, tuple))
|
|
259
|
+
and len(decoded) == 2
|
|
260
|
+
and isinstance(decoded[0], str)
|
|
261
|
+
and isinstance(decoded[1], (bytes, bytearray))
|
|
262
|
+
):
|
|
263
|
+
tag, payload = decoded
|
|
264
|
+
if tag:
|
|
265
|
+
resolved_cls = _resolve_class(tag)
|
|
266
|
+
if resolved_cls is not None:
|
|
267
|
+
return msgspec.msgpack.decode(payload, type=resolved_cls, dec_hook=_dec_hook)
|
|
268
|
+
return msgspec.msgpack.decode(payload, dec_hook=_dec_hook)
|
|
269
|
+
return decoded
|
|
270
|
+
except Exception:
|
|
271
|
+
return msgspec.msgpack.decode(data, dec_hook=_dec_hook)
|
|
185
272
|
|
|
186
273
|
|
|
187
274
|
__all__ = [
|
polyxml-0.5.0/README.md
DELETED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|