polyxml 0.4.0__tar.gz → 0.5.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.4.0 → polyxml-0.5.0}/Cargo.lock +4 -4
- {polyxml-0.4.0 → polyxml-0.5.0}/Cargo.toml +1 -1
- {polyxml-0.4.0 → polyxml-0.5.0}/PKG-INFO +4 -1
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-python/tests/test_polyxml.py +63 -7
- {polyxml-0.4.0 → polyxml-0.5.0}/pyproject.toml +5 -1
- polyxml-0.5.0/python/polyxml/AGENT_GUIDE.md +89 -0
- polyxml-0.5.0/python/polyxml/__init__.py +194 -0
- polyxml-0.4.0/python/polyxml/__init__.py +0 -87
- {polyxml-0.4.0 → polyxml-0.5.0}/README.md +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/Cargo.toml +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/README.md +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/benches/core_benchmarks.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/converters.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/error.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/lib.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/parser.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/schema.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/serializer.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/src/value.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/tests/test_core.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-core/tests/test_security.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-python/Cargo.toml +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-python/README.md +0 -0
- {polyxml-0.4.0 → polyxml-0.5.0}/crates/polyxml-python/src/lib.rs +0 -0
- {polyxml-0.4.0 → polyxml-0.5.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.5.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.5.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.5.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.5.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.5.0
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -16,7 +16,10 @@ Requires-Dist: pytest>=8.0.0 ; extra == 'dev'
|
|
|
16
16
|
Requires-Dist: pytest-cov>=5.0.0 ; extra == 'dev'
|
|
17
17
|
Requires-Dist: pydantic>=2.7.0 ; extra == 'dev'
|
|
18
18
|
Requires-Dist: ruff>=0.4.0 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: msgspec>=0.18.0 ; extra == 'dev'
|
|
20
|
+
Requires-Dist: msgspec>=0.18.0 ; extra == 'msgpack'
|
|
19
21
|
Provides-Extra: dev
|
|
22
|
+
Provides-Extra: msgpack
|
|
20
23
|
Summary: High-performance, polyglot XML data-binding engine built in Rust
|
|
21
24
|
Author-email: Bailey Nguyen <bailey.tan.nguyen@gmail.com>
|
|
22
25
|
License: MIT
|
|
@@ -12,7 +12,6 @@ try:
|
|
|
12
12
|
except ImportError:
|
|
13
13
|
|
|
14
14
|
class XmlDate:
|
|
15
|
-
|
|
16
15
|
def __init__(self, val: str):
|
|
17
16
|
self.val = val
|
|
18
17
|
|
|
@@ -27,7 +26,6 @@ except ImportError:
|
|
|
27
26
|
return self.val
|
|
28
27
|
|
|
29
28
|
class XmlDateTime:
|
|
30
|
-
|
|
31
29
|
def __init__(self, val: str):
|
|
32
30
|
self.val = val
|
|
33
31
|
|
|
@@ -42,7 +40,6 @@ except ImportError:
|
|
|
42
40
|
return self.val
|
|
43
41
|
|
|
44
42
|
class XmlTime:
|
|
45
|
-
|
|
46
43
|
def __init__(self, val: str):
|
|
47
44
|
self.val = val
|
|
48
45
|
|
|
@@ -57,7 +54,6 @@ except ImportError:
|
|
|
57
54
|
return self.val
|
|
58
55
|
|
|
59
56
|
class XmlDuration:
|
|
60
|
-
|
|
61
57
|
def __init__(self, val: str):
|
|
62
58
|
self.val = val
|
|
63
59
|
|
|
@@ -67,6 +63,7 @@ except ImportError:
|
|
|
67
63
|
def __str__(self):
|
|
68
64
|
return self.val
|
|
69
65
|
|
|
66
|
+
|
|
70
67
|
import polyxml
|
|
71
68
|
|
|
72
69
|
|
|
@@ -333,9 +330,7 @@ def test_to_bytes_invalid_type():
|
|
|
333
330
|
|
|
334
331
|
@dataclass
|
|
335
332
|
class UnionWrapper:
|
|
336
|
-
child: None | ChildNode = field(
|
|
337
|
-
default=None, metadata={"type": "Element", "name": "child"}
|
|
338
|
-
)
|
|
333
|
+
child: None | ChildNode = field(default=None, metadata={"type": "Element", "name": "child"})
|
|
339
334
|
count: int | None = field(default=None, metadata={"type": "Element"})
|
|
340
335
|
|
|
341
336
|
|
|
@@ -346,3 +341,64 @@ def test_pep604_union_nested_dataclass():
|
|
|
346
341
|
assert res.child.tag == "Payload"
|
|
347
342
|
assert res.count == 42
|
|
348
343
|
|
|
344
|
+
|
|
345
|
+
def test_binary_serialization_dataclass():
|
|
346
|
+
item = SimpleItem(id=42, name="TestItem", price=19.99, active=True)
|
|
347
|
+
payload = polyxml.dumps_binary(item)
|
|
348
|
+
assert isinstance(payload, bytes)
|
|
349
|
+
assert len(payload) > 0
|
|
350
|
+
|
|
351
|
+
decoded = polyxml.loads_binary(payload, SimpleItem)
|
|
352
|
+
assert isinstance(decoded, SimpleItem)
|
|
353
|
+
assert decoded.id == 42
|
|
354
|
+
assert decoded.name == "TestItem"
|
|
355
|
+
assert decoded.price == 19.99
|
|
356
|
+
assert decoded.active is True
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_binary_serialization_decimal():
|
|
360
|
+
@dataclass
|
|
361
|
+
class DecimalItem:
|
|
362
|
+
val: Decimal
|
|
363
|
+
|
|
364
|
+
item = DecimalItem(val=Decimal("19.99"))
|
|
365
|
+
payload = polyxml.dumps_binary(item)
|
|
366
|
+
decoded = polyxml.loads_binary(payload, DecimalItem)
|
|
367
|
+
assert decoded.val == Decimal("19.99")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def test_binary_serialization_untyped():
|
|
371
|
+
data = {"name": "Alice", "score": 100}
|
|
372
|
+
payload = polyxml.dumps_binary(data)
|
|
373
|
+
decoded = polyxml.loads_binary(payload)
|
|
374
|
+
assert decoded == data
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def test_binary_serialization_xmldate_and_path():
|
|
378
|
+
@dataclass
|
|
379
|
+
class ExtraItem:
|
|
380
|
+
date: XmlDate
|
|
381
|
+
path: pathlib.Path
|
|
382
|
+
|
|
383
|
+
orig = ExtraItem(date=XmlDate("2026-09-12"), path=pathlib.Path("/tmp/test.xml"))
|
|
384
|
+
payload = polyxml.dumps_binary(orig)
|
|
385
|
+
decoded = polyxml.loads_binary(payload, ExtraItem)
|
|
386
|
+
assert decoded.date == XmlDate("2026-09-12")
|
|
387
|
+
assert decoded.path == pathlib.Path("/tmp/test.xml")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_binary_serialization_unsupported_type():
|
|
391
|
+
class CustomObject:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
with pytest.raises(NotImplementedError, match="PolyXML binary serializer cannot serialize"):
|
|
395
|
+
polyxml.dumps_binary(CustomObject())
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_binary_deserialization_unsupported_type():
|
|
399
|
+
class UnhandledClass:
|
|
400
|
+
pass
|
|
401
|
+
|
|
402
|
+
payload = polyxml.dumps_binary({"a": 1})
|
|
403
|
+
with pytest.raises(NotImplementedError, match="PolyXML binary deserializer cannot deserialize"):
|
|
404
|
+
polyxml.loads_binary(payload, UnhandledClass)
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "polyxml"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "High-performance, polyglot XML data-binding engine built in Rust"
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Bailey Nguyen", email = "bailey.tan.nguyen@gmail.com" }
|
|
@@ -28,11 +28,15 @@ classifiers = [
|
|
|
28
28
|
dependencies = []
|
|
29
29
|
|
|
30
30
|
[project.optional-dependencies]
|
|
31
|
+
msgpack = [
|
|
32
|
+
"msgspec>=0.18.0",
|
|
33
|
+
]
|
|
31
34
|
dev = [
|
|
32
35
|
"pytest>=8.0.0",
|
|
33
36
|
"pytest-cov>=5.0.0",
|
|
34
37
|
"pydantic>=2.7.0",
|
|
35
38
|
"ruff>=0.4.0",
|
|
39
|
+
"msgspec>=0.18.0",
|
|
36
40
|
]
|
|
37
41
|
|
|
38
42
|
[project.urls]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# PolyXML AI Agent Guide
|
|
2
|
+
|
|
3
|
+
Quick-reference instructions for AI coding assistants (Claude, Cursor, Copilot, Antigravity, etc.) using `polyxml` in Python projects.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Overview & Capabilities
|
|
8
|
+
|
|
9
|
+
`polyxml` is an ultra-fast, native XML data-binding engine written in pure Rust (using `quick-xml` and `lexical-core`) with stable ABI (`abi3-py312`) Python bindings. It provides bidirectional deserialization and serialization for Python 3.12+ `dataclasses` and Pydantic v2 models.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 2. Common Recipes
|
|
14
|
+
|
|
15
|
+
### A. Deserializing XML (Strings, Bytes, or Files)
|
|
16
|
+
```python
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
import pathlib
|
|
19
|
+
import polyxml
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class Item:
|
|
24
|
+
id: int = field(metadata={"type": "Attribute"})
|
|
25
|
+
name: str = field(metadata={"type": "Element"})
|
|
26
|
+
price: float = field(metadata={"type": "Element"})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# 1. From raw XML string:
|
|
30
|
+
item = polyxml.deserialize('<Item id="1"><name>Bolt</name><price>0.99</price></Item>', Item)
|
|
31
|
+
|
|
32
|
+
# 2. From raw bytes:
|
|
33
|
+
item = polyxml.deserialize(b"<Item ...>...</Item>", Item)
|
|
34
|
+
|
|
35
|
+
# 3. From pathlib.Path:
|
|
36
|
+
item = polyxml.deserialize(pathlib.Path("item.xml"), Item)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### B. Streaming Massive XML Files with O(1) Constant Memory
|
|
40
|
+
For large datasets (hundreds of MBs or GBs such as transit timetables or feeds), do **NOT** load the entire XML tree at once. Use `iterparse`:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import polyxml
|
|
44
|
+
from my_models import VehicleJourney
|
|
45
|
+
|
|
46
|
+
# Streams elements one-by-one with zero intermediate DOM allocations
|
|
47
|
+
for journey in polyxml.iterparse("large_transit_dump.xml", VehicleJourney, tag="VehicleJourney"):
|
|
48
|
+
process_journey(journey)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### C. Pydantic v2 Models
|
|
52
|
+
PolyXML natively deserializes into Pydantic v2 `BaseModel` classes with the same API:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pydantic import BaseModel, Field
|
|
56
|
+
import polyxml
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class StopPoint(BaseModel):
|
|
60
|
+
id: str = Field(json_schema_extra={"type": "Attribute"})
|
|
61
|
+
name: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
stop = polyxml.deserialize(
|
|
65
|
+
'<StopPoint id="SP1"><name>Central Station</name></StopPoint>', StopPoint
|
|
66
|
+
)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### D. Serializing to XML
|
|
70
|
+
```python
|
|
71
|
+
import polyxml
|
|
72
|
+
|
|
73
|
+
# Serialize model to UTF-8 XML bytes
|
|
74
|
+
xml_bytes = polyxml.serialize(item, indent=2)
|
|
75
|
+
|
|
76
|
+
# If string is required:
|
|
77
|
+
xml_string = xml_bytes.decode("utf-8")
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 3. Common Pitfalls & Quick Fixes
|
|
83
|
+
|
|
84
|
+
- **Pitfall**: Attempting `polyxml.XmlParser()`
|
|
85
|
+
**Fix**: PolyXML uses module-level functional entrypoints: `polyxml.deserialize(...)` and `polyxml.iterparse(...)`.
|
|
86
|
+
- **Pitfall**: Passing file handles or paths without reading
|
|
87
|
+
**Fix**: `polyxml.deserialize(...)` accepts strings, byte buffers, `pathlib.Path`, or `IO` streams directly.
|
|
88
|
+
- **Pitfall**: High memory usage on massive XML files
|
|
89
|
+
**Fix**: Switch from `polyxml.deserialize` to `polyxml.iterparse(source, ModelClass, tag="TagName")`.
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""PolyXML: High-performance, polyglot native XML data-binding engine.
|
|
2
|
+
|
|
3
|
+
QUICKSTART FOR AI AGENTS & DEVELOPERS:
|
|
4
|
+
|
|
5
|
+
1. Deserialize XML (string, bytes, or file Path) into a typed Dataclass or Pydantic model:
|
|
6
|
+
>>> import polyxml
|
|
7
|
+
>>> from dataclasses import dataclass, field
|
|
8
|
+
>>>
|
|
9
|
+
>>> @dataclass
|
|
10
|
+
>>> class User:
|
|
11
|
+
... name: str = field(metadata={"type": "Element"})
|
|
12
|
+
>>>
|
|
13
|
+
>>> user = polyxml.deserialize("<User><name>Alice</name></User>", User)
|
|
14
|
+
>>> user.name
|
|
15
|
+
'Alice'
|
|
16
|
+
|
|
17
|
+
2. Stream large XML files with O(1) constant memory (Streaming / Facet Extraction):
|
|
18
|
+
>>> for user in polyxml.iterparse("massive_users.xml", User, tag="User"):
|
|
19
|
+
... process(user)
|
|
20
|
+
|
|
21
|
+
3. Serialize a model instance back into XML bytes:
|
|
22
|
+
>>> xml_bytes = polyxml.serialize(user, indent=2)
|
|
23
|
+
|
|
24
|
+
See `AGENT_GUIDE.md` or https://nth-bailey.github.io/PolyXML/ for complete documentation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import pathlib
|
|
28
|
+
from collections.abc import Iterator
|
|
29
|
+
from decimal import Decimal
|
|
30
|
+
from typing import IO
|
|
31
|
+
|
|
32
|
+
from polyxml._polyxml import ( # type: ignore[import-not-found]
|
|
33
|
+
deserialize as _deserialize,
|
|
34
|
+
)
|
|
35
|
+
from polyxml._polyxml import (
|
|
36
|
+
iterparse as _iterparse,
|
|
37
|
+
)
|
|
38
|
+
from polyxml._polyxml import (
|
|
39
|
+
serialize as _serialize,
|
|
40
|
+
)
|
|
41
|
+
from polyxml._polyxml import (
|
|
42
|
+
version as _version,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__version__: str = _version()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _to_bytes(source: bytes | str | pathlib.Path | IO[bytes] | IO[str]) -> bytes:
|
|
49
|
+
if isinstance(source, bytes):
|
|
50
|
+
return source
|
|
51
|
+
if isinstance(source, str):
|
|
52
|
+
if source.lstrip().startswith("<"):
|
|
53
|
+
return source.encode("utf-8")
|
|
54
|
+
path = pathlib.Path(source)
|
|
55
|
+
if path.is_file():
|
|
56
|
+
return path.read_bytes()
|
|
57
|
+
return source.encode("utf-8")
|
|
58
|
+
if isinstance(source, pathlib.Path):
|
|
59
|
+
return source.read_bytes()
|
|
60
|
+
if hasattr(source, "read"):
|
|
61
|
+
data = source.read()
|
|
62
|
+
return data.encode("utf-8") if isinstance(data, str) else data
|
|
63
|
+
raise TypeError(
|
|
64
|
+
f"Unsupported XML source type: {type(source).__name__}. "
|
|
65
|
+
"Expected bytes, str (XML string or file path), pathlib.Path, or a binary/text IO stream."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def deserialize[T](
|
|
70
|
+
source: bytes | str | pathlib.Path | IO[bytes] | IO[str], target_type: type[T]
|
|
71
|
+
) -> T:
|
|
72
|
+
"""Deserialize XML bytes, string, file path, or stream into a typed model.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
source: XML content as raw bytes, string, Path, or file stream.
|
|
76
|
+
target_type: The target dataclass or model class.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The deserialized model instance.
|
|
80
|
+
"""
|
|
81
|
+
return _deserialize(_to_bytes(source), target_type)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def iterparse[T](
|
|
85
|
+
source: bytes | str | pathlib.Path | IO[bytes] | IO[str],
|
|
86
|
+
target_type: type[T],
|
|
87
|
+
tag: str | None = None,
|
|
88
|
+
) -> Iterator[T]:
|
|
89
|
+
"""Stream and deserialize XML elements one-by-one with O(1) constant memory.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
source: XML content as raw bytes, string, Path, or file stream.
|
|
93
|
+
target_type: The target dataclass or model class for each element.
|
|
94
|
+
tag: Optional XML element tag name to match. Defaults to the model schema name.
|
|
95
|
+
|
|
96
|
+
Yields:
|
|
97
|
+
Deserialized model instances as they are streamed.
|
|
98
|
+
"""
|
|
99
|
+
return _iterparse(_to_bytes(source), target_type, tag)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def serialize(obj: object, *, indent: int | None = None) -> bytes:
|
|
103
|
+
"""Serialize a strongly-typed model instance back into XML bytes.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
obj: Python dataclass or Pydantic model instance.
|
|
107
|
+
indent: Optional indentation size in spaces for pretty-printing.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
UTF-8 encoded XML bytes representing the model instance.
|
|
111
|
+
"""
|
|
112
|
+
return _serialize(obj, indent=indent)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
import msgspec
|
|
117
|
+
|
|
118
|
+
_HAS_MSGSPEC = True
|
|
119
|
+
except ImportError: # pragma: no cover
|
|
120
|
+
msgspec = None # type: ignore[assignment]
|
|
121
|
+
_HAS_MSGSPEC = False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _enc_hook(obj: object) -> str:
|
|
125
|
+
if isinstance(obj, pathlib.Path | Decimal):
|
|
126
|
+
return str(obj)
|
|
127
|
+
if hasattr(obj, "from_string") and hasattr(obj, "val"):
|
|
128
|
+
return str(obj)
|
|
129
|
+
raise NotImplementedError(
|
|
130
|
+
f"PolyXML binary serializer cannot serialize object of type {type(obj).__name__}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _dec_hook(target_type: type[object], obj: object) -> object:
|
|
135
|
+
if target_type is pathlib.Path:
|
|
136
|
+
return pathlib.Path(str(obj))
|
|
137
|
+
if hasattr(target_type, "from_string"):
|
|
138
|
+
return target_type.from_string(str(obj))
|
|
139
|
+
raise NotImplementedError(
|
|
140
|
+
f"PolyXML binary deserializer cannot deserialize object into type {target_type.__name__}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def dumps_binary(obj: object) -> bytes:
|
|
145
|
+
"""Serialize a model or dataclass into a high-throughput binary MessagePack buffer.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
obj: Python dataclass, Pydantic model, or object.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Compact binary MessagePack bytes.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
RuntimeError: If msgspec is not installed.
|
|
155
|
+
"""
|
|
156
|
+
if not _HAS_MSGSPEC: # pragma: no cover
|
|
157
|
+
raise RuntimeError(
|
|
158
|
+
"PolyXML binary serialization requires 'msgspec'. "
|
|
159
|
+
"Install it via: pip install 'polyxml[msgpack]' or pip install msgspec"
|
|
160
|
+
)
|
|
161
|
+
return msgspec.msgpack.encode(obj, enc_hook=_enc_hook)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def loads_binary[T](data: bytes, target_type: type[T] | None = None) -> T | object:
|
|
165
|
+
"""Deserialize binary MessagePack bytes into a strongly-typed model or object.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
data: Binary MessagePack bytes to decode.
|
|
169
|
+
target_type: Optional target dataclass or model class. If omitted, returns dynamic structure.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The deserialized model instance or object.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
RuntimeError: If msgspec is not installed.
|
|
176
|
+
"""
|
|
177
|
+
if not _HAS_MSGSPEC: # pragma: no cover
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"PolyXML binary deserialization requires 'msgspec'. "
|
|
180
|
+
"Install it via: pip install 'polyxml[msgpack]' or pip install msgspec"
|
|
181
|
+
)
|
|
182
|
+
if target_type is not None:
|
|
183
|
+
return msgspec.msgpack.decode(data, type=target_type, dec_hook=_dec_hook)
|
|
184
|
+
return msgspec.msgpack.decode(data, dec_hook=_dec_hook)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
__all__ = [
|
|
188
|
+
"__version__",
|
|
189
|
+
"deserialize",
|
|
190
|
+
"dumps_binary",
|
|
191
|
+
"iterparse",
|
|
192
|
+
"loads_binary",
|
|
193
|
+
"serialize",
|
|
194
|
+
]
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
"""PolyXML: High-performance, polyglot XML data-binding engine."""
|
|
2
|
-
|
|
3
|
-
import pathlib
|
|
4
|
-
from collections.abc import Iterator
|
|
5
|
-
from typing import IO
|
|
6
|
-
|
|
7
|
-
from polyxml._polyxml import ( # type: ignore[import-not-found]
|
|
8
|
-
deserialize as _deserialize,
|
|
9
|
-
)
|
|
10
|
-
from polyxml._polyxml import (
|
|
11
|
-
iterparse as _iterparse,
|
|
12
|
-
)
|
|
13
|
-
from polyxml._polyxml import (
|
|
14
|
-
serialize as _serialize,
|
|
15
|
-
)
|
|
16
|
-
from polyxml._polyxml import (
|
|
17
|
-
version as _version,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
__version__: str = _version()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _to_bytes(source: bytes | str | pathlib.Path | IO[bytes] | IO[str]) -> bytes:
|
|
24
|
-
if isinstance(source, bytes):
|
|
25
|
-
return source
|
|
26
|
-
if isinstance(source, str):
|
|
27
|
-
if source.lstrip().startswith("<"):
|
|
28
|
-
return source.encode("utf-8")
|
|
29
|
-
path = pathlib.Path(source)
|
|
30
|
-
if path.is_file():
|
|
31
|
-
return path.read_bytes()
|
|
32
|
-
return source.encode("utf-8")
|
|
33
|
-
if isinstance(source, pathlib.Path):
|
|
34
|
-
return source.read_bytes()
|
|
35
|
-
if hasattr(source, "read"):
|
|
36
|
-
data = source.read()
|
|
37
|
-
return data.encode("utf-8") if isinstance(data, str) else data
|
|
38
|
-
raise TypeError(f"Unsupported XML source type: {type(source)}")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def deserialize[T](
|
|
42
|
-
source: bytes | str | pathlib.Path | IO[bytes] | IO[str], target_type: type[T]
|
|
43
|
-
) -> T:
|
|
44
|
-
"""Deserialize XML bytes, string, file path, or stream into a typed model.
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
source: XML content as raw bytes, string, Path, or file stream.
|
|
48
|
-
target_type: The target dataclass or model class.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
The deserialized model instance.
|
|
52
|
-
"""
|
|
53
|
-
return _deserialize(_to_bytes(source), target_type)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def iterparse[T](
|
|
57
|
-
source: bytes | str | pathlib.Path | IO[bytes] | IO[str],
|
|
58
|
-
target_type: type[T],
|
|
59
|
-
tag: str | None = None,
|
|
60
|
-
) -> Iterator[T]:
|
|
61
|
-
"""Stream and deserialize XML elements one-by-one with O(1) constant memory.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
source: XML content as raw bytes, string, Path, or file stream.
|
|
65
|
-
target_type: The target dataclass or model class for each element.
|
|
66
|
-
tag: Optional XML element tag name to match. Defaults to the model schema name.
|
|
67
|
-
|
|
68
|
-
Yields:
|
|
69
|
-
Deserialized model instances as they are streamed.
|
|
70
|
-
"""
|
|
71
|
-
return _iterparse(_to_bytes(source), target_type, tag)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def serialize(obj: object, *, indent: int | None = None) -> bytes:
|
|
75
|
-
"""Serialize a strongly-typed model instance back into XML bytes.
|
|
76
|
-
|
|
77
|
-
Args:
|
|
78
|
-
obj: Python dataclass or Pydantic model instance.
|
|
79
|
-
indent: Optional indentation size in spaces for pretty-printing.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
UTF-8 encoded XML bytes representing the model instance.
|
|
83
|
-
"""
|
|
84
|
-
return _serialize(obj, indent=indent)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
__all__ = ["__version__", "deserialize", "iterparse", "serialize"]
|
|
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
|
|
File without changes
|