exonware-xwsystem 0.0.1.408__py3-none-any.whl → 0.0.1.410__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.
- exonware/__init__.py +2 -2
- exonware/conf.py +10 -20
- exonware/xwsystem/__init__.py +5 -15
- exonware/xwsystem/caching/__init__.py +1 -1
- exonware/xwsystem/caching/base.py +15 -15
- exonware/xwsystem/caching/bloom_cache.py +4 -4
- exonware/xwsystem/caching/cache_manager.py +2 -2
- exonware/xwsystem/caching/conditional.py +3 -3
- exonware/xwsystem/caching/contracts.py +12 -12
- exonware/xwsystem/caching/decorators.py +1 -1
- exonware/xwsystem/caching/defs.py +1 -1
- exonware/xwsystem/caching/disk_cache.py +4 -4
- exonware/xwsystem/caching/distributed.py +1 -1
- exonware/xwsystem/caching/errors.py +1 -1
- exonware/xwsystem/caching/events.py +7 -7
- exonware/xwsystem/caching/eviction_strategies.py +9 -9
- exonware/xwsystem/caching/fluent.py +1 -1
- exonware/xwsystem/caching/integrity.py +1 -1
- exonware/xwsystem/caching/lfu_cache.py +23 -8
- exonware/xwsystem/caching/lfu_optimized.py +20 -20
- exonware/xwsystem/caching/lru_cache.py +13 -6
- exonware/xwsystem/caching/memory_bounded.py +7 -7
- exonware/xwsystem/caching/metrics_exporter.py +5 -5
- exonware/xwsystem/caching/observable_cache.py +1 -1
- exonware/xwsystem/caching/pluggable_cache.py +8 -8
- exonware/xwsystem/caching/rate_limiter.py +1 -1
- exonware/xwsystem/caching/read_through.py +5 -5
- exonware/xwsystem/caching/secure_cache.py +1 -1
- exonware/xwsystem/caching/serializable.py +2 -2
- exonware/xwsystem/caching/stats.py +7 -7
- exonware/xwsystem/caching/tagging.py +10 -10
- exonware/xwsystem/caching/ttl_cache.py +21 -6
- exonware/xwsystem/caching/two_tier_cache.py +5 -5
- exonware/xwsystem/caching/utils.py +3 -3
- exonware/xwsystem/caching/validation.py +1 -1
- exonware/xwsystem/caching/warming.py +8 -8
- exonware/xwsystem/caching/write_behind.py +4 -4
- exonware/xwsystem/cli/__init__.py +1 -1
- exonware/xwsystem/cli/args.py +10 -10
- exonware/xwsystem/cli/base.py +15 -15
- exonware/xwsystem/cli/colors.py +1 -1
- exonware/xwsystem/cli/console.py +1 -1
- exonware/xwsystem/cli/contracts.py +5 -5
- exonware/xwsystem/cli/defs.py +1 -1
- exonware/xwsystem/cli/errors.py +1 -1
- exonware/xwsystem/cli/progress.py +1 -1
- exonware/xwsystem/cli/prompts.py +1 -1
- exonware/xwsystem/cli/tables.py +7 -7
- exonware/xwsystem/config/__init__.py +1 -1
- exonware/xwsystem/config/base.py +13 -13
- exonware/xwsystem/config/contracts.py +22 -22
- exonware/xwsystem/config/defaults.py +2 -2
- exonware/xwsystem/config/defs.py +1 -1
- exonware/xwsystem/config/errors.py +1 -1
- exonware/xwsystem/config/logging.py +1 -1
- exonware/xwsystem/config/logging_setup.py +1 -1
- exonware/xwsystem/config/performance.py +7 -7
- exonware/xwsystem/config/performance_modes.py +20 -20
- exonware/xwsystem/config/version_manager.py +4 -4
- exonware/xwsystem/{http → http_client}/__init__.py +1 -1
- exonware/xwsystem/{http → http_client}/advanced_client.py +20 -20
- exonware/xwsystem/{http → http_client}/base.py +12 -12
- exonware/xwsystem/{http → http_client}/client.py +43 -43
- exonware/xwsystem/{http → http_client}/contracts.py +5 -5
- exonware/xwsystem/{http → http_client}/defs.py +2 -2
- exonware/xwsystem/{http → http_client}/errors.py +1 -1
- exonware/xwsystem/io/__init__.py +1 -1
- exonware/xwsystem/io/archive/__init__.py +1 -1
- exonware/xwsystem/io/archive/archive.py +5 -5
- exonware/xwsystem/io/archive/archive_files.py +8 -8
- exonware/xwsystem/io/archive/archivers.py +2 -2
- exonware/xwsystem/io/archive/base.py +15 -15
- exonware/xwsystem/io/archive/codec_integration.py +1 -1
- exonware/xwsystem/io/archive/compression.py +1 -1
- exonware/xwsystem/io/archive/formats/__init__.py +1 -1
- exonware/xwsystem/io/archive/formats/brotli_format.py +7 -7
- exonware/xwsystem/io/archive/formats/lz4_format.py +7 -7
- exonware/xwsystem/io/archive/formats/rar.py +7 -7
- exonware/xwsystem/io/archive/formats/sevenzip.py +7 -7
- exonware/xwsystem/io/archive/formats/squashfs_format.py +7 -7
- exonware/xwsystem/io/archive/formats/tar.py +8 -8
- exonware/xwsystem/io/archive/formats/wim_format.py +7 -7
- exonware/xwsystem/io/archive/formats/zip.py +8 -8
- exonware/xwsystem/io/archive/formats/zpaq_format.py +7 -7
- exonware/xwsystem/io/archive/formats/zstandard.py +7 -7
- exonware/xwsystem/io/base.py +17 -17
- exonware/xwsystem/io/codec/__init__.py +1 -1
- exonware/xwsystem/io/codec/base.py +260 -13
- exonware/xwsystem/io/codec/contracts.py +3 -6
- exonware/xwsystem/io/codec/registry.py +28 -28
- exonware/xwsystem/io/common/__init__.py +1 -1
- exonware/xwsystem/io/common/atomic.py +2 -2
- exonware/xwsystem/io/common/base.py +1 -1
- exonware/xwsystem/io/common/lock.py +1 -1
- exonware/xwsystem/io/common/watcher.py +4 -4
- exonware/xwsystem/io/contracts.py +34 -39
- exonware/xwsystem/io/defs.py +2 -2
- exonware/xwsystem/io/errors.py +32 -3
- exonware/xwsystem/io/facade.py +3 -3
- exonware/xwsystem/io/file/__init__.py +1 -1
- exonware/xwsystem/io/file/base.py +2 -2
- exonware/xwsystem/io/file/conversion.py +1 -1
- exonware/xwsystem/io/file/file.py +3 -3
- exonware/xwsystem/io/file/paged_source.py +1 -1
- exonware/xwsystem/io/file/paging/__init__.py +1 -1
- exonware/xwsystem/io/file/paging/byte_paging.py +1 -1
- exonware/xwsystem/io/file/paging/line_paging.py +1 -1
- exonware/xwsystem/io/file/paging/record_paging.py +1 -1
- exonware/xwsystem/io/file/paging/registry.py +4 -4
- exonware/xwsystem/io/file/source.py +3 -3
- exonware/xwsystem/io/filesystem/__init__.py +1 -1
- exonware/xwsystem/io/filesystem/base.py +1 -1
- exonware/xwsystem/io/filesystem/local.py +1 -1
- exonware/xwsystem/io/folder/__init__.py +1 -1
- exonware/xwsystem/io/folder/base.py +2 -2
- exonware/xwsystem/io/folder/folder.py +5 -5
- exonware/xwsystem/io/serialization/__init__.py +1 -1
- exonware/xwsystem/io/serialization/auto_serializer.py +3 -3
- exonware/xwsystem/io/serialization/base.py +84 -35
- exonware/xwsystem/io/serialization/contracts.py +6 -4
- exonware/xwsystem/io/serialization/defs.py +1 -1
- exonware/xwsystem/io/serialization/errors.py +1 -1
- exonware/xwsystem/io/serialization/flyweight.py +18 -18
- exonware/xwsystem/io/serialization/format_detector.py +11 -11
- exonware/xwsystem/io/serialization/formats/__init__.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/bson.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/cbor.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/marshal.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/msgpack.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/pickle.py +1 -1
- exonware/xwsystem/io/serialization/formats/binary/plistlib.py +1 -1
- exonware/xwsystem/io/serialization/formats/database/dbm.py +1 -1
- exonware/xwsystem/io/serialization/formats/database/shelve.py +1 -1
- exonware/xwsystem/io/serialization/formats/database/sqlite3.py +1 -1
- exonware/xwsystem/io/serialization/formats/text/configparser.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/csv.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/formdata.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/json.py +23 -5
- exonware/xwsystem/io/serialization/formats/text/json5.py +93 -10
- exonware/xwsystem/io/serialization/formats/text/jsonlines.py +4 -4
- exonware/xwsystem/io/serialization/formats/text/multipart.py +2 -2
- exonware/xwsystem/io/serialization/formats/text/toml.py +47 -2
- exonware/xwsystem/io/serialization/formats/text/xml.py +444 -69
- exonware/xwsystem/io/serialization/formats/text/yaml.py +1 -1
- exonware/xwsystem/io/serialization/registry.py +5 -5
- exonware/xwsystem/io/serialization/serializer.py +11 -11
- exonware/xwsystem/io/serialization/utils/__init__.py +1 -1
- exonware/xwsystem/io/serialization/utils/path_ops.py +3 -3
- exonware/xwsystem/io/stream/__init__.py +1 -1
- exonware/xwsystem/io/stream/async_operations.py +3 -3
- exonware/xwsystem/io/stream/base.py +3 -7
- exonware/xwsystem/io/stream/codec_io.py +4 -7
- exonware/xwsystem/ipc/async_fabric.py +7 -7
- exonware/xwsystem/ipc/base.py +8 -8
- exonware/xwsystem/ipc/contracts.py +4 -4
- exonware/xwsystem/ipc/defs.py +1 -1
- exonware/xwsystem/ipc/errors.py +1 -1
- exonware/xwsystem/ipc/message_queue.py +4 -6
- exonware/xwsystem/ipc/process_manager.py +7 -7
- exonware/xwsystem/ipc/process_pool.py +8 -8
- exonware/xwsystem/ipc/shared_memory.py +5 -5
- exonware/xwsystem/monitoring/base.py +32 -32
- exonware/xwsystem/monitoring/contracts.py +27 -27
- exonware/xwsystem/monitoring/defs.py +1 -1
- exonware/xwsystem/monitoring/error_recovery.py +15 -15
- exonware/xwsystem/monitoring/errors.py +1 -1
- exonware/xwsystem/monitoring/memory_monitor.py +11 -11
- exonware/xwsystem/monitoring/metrics.py +8 -8
- exonware/xwsystem/monitoring/performance_manager_generic.py +19 -19
- exonware/xwsystem/monitoring/performance_monitor.py +11 -11
- exonware/xwsystem/monitoring/performance_validator.py +20 -20
- exonware/xwsystem/monitoring/system_monitor.py +16 -16
- exonware/xwsystem/monitoring/tracing.py +19 -19
- exonware/xwsystem/monitoring/tracker.py +7 -7
- exonware/xwsystem/operations/__init__.py +5 -5
- exonware/xwsystem/operations/base.py +3 -3
- exonware/xwsystem/operations/contracts.py +3 -3
- exonware/xwsystem/operations/defs.py +5 -5
- exonware/xwsystem/operations/diff.py +5 -5
- exonware/xwsystem/operations/merge.py +2 -2
- exonware/xwsystem/operations/patch.py +5 -5
- exonware/xwsystem/patterns/base.py +3 -3
- exonware/xwsystem/patterns/context_manager.py +6 -6
- exonware/xwsystem/patterns/contracts.py +22 -24
- exonware/xwsystem/patterns/defs.py +1 -1
- exonware/xwsystem/patterns/dynamic_facade.py +5 -5
- exonware/xwsystem/patterns/errors.py +7 -7
- exonware/xwsystem/patterns/handler_factory.py +11 -10
- exonware/xwsystem/patterns/import_registry.py +22 -22
- exonware/xwsystem/patterns/object_pool.py +11 -10
- exonware/xwsystem/patterns/registry.py +44 -31
- exonware/xwsystem/plugins/__init__.py +1 -1
- exonware/xwsystem/plugins/base.py +23 -23
- exonware/xwsystem/plugins/contracts.py +26 -26
- exonware/xwsystem/plugins/defs.py +1 -1
- exonware/xwsystem/plugins/errors.py +7 -7
- exonware/xwsystem/runtime/__init__.py +1 -1
- exonware/xwsystem/runtime/base.py +40 -40
- exonware/xwsystem/runtime/contracts.py +8 -8
- exonware/xwsystem/runtime/defs.py +1 -1
- exonware/xwsystem/runtime/env.py +8 -8
- exonware/xwsystem/runtime/errors.py +1 -1
- exonware/xwsystem/runtime/reflection.py +13 -13
- exonware/xwsystem/security/auth.py +47 -15
- exonware/xwsystem/security/base.py +16 -16
- exonware/xwsystem/security/contracts.py +30 -30
- exonware/xwsystem/security/crypto.py +7 -7
- exonware/xwsystem/security/defs.py +1 -1
- exonware/xwsystem/security/errors.py +1 -1
- exonware/xwsystem/security/hazmat.py +6 -6
- exonware/xwsystem/security/path_validator.py +1 -1
- exonware/xwsystem/shared/__init__.py +1 -1
- exonware/xwsystem/shared/base.py +14 -14
- exonware/xwsystem/shared/contracts.py +6 -6
- exonware/xwsystem/shared/defs.py +1 -1
- exonware/xwsystem/shared/errors.py +1 -1
- exonware/xwsystem/structures/base.py +28 -28
- exonware/xwsystem/structures/circular_detector.py +15 -15
- exonware/xwsystem/structures/contracts.py +9 -9
- exonware/xwsystem/structures/defs.py +1 -1
- exonware/xwsystem/structures/errors.py +1 -1
- exonware/xwsystem/structures/tree_walker.py +8 -8
- exonware/xwsystem/threading/async_primitives.py +6 -6
- exonware/xwsystem/threading/base.py +18 -18
- exonware/xwsystem/threading/contracts.py +13 -13
- exonware/xwsystem/threading/defs.py +1 -1
- exonware/xwsystem/threading/errors.py +1 -1
- exonware/xwsystem/threading/safe_factory.py +10 -9
- exonware/xwsystem/utils/base.py +33 -33
- exonware/xwsystem/utils/contracts.py +9 -9
- exonware/xwsystem/utils/dt/__init__.py +1 -1
- exonware/xwsystem/utils/dt/base.py +5 -5
- exonware/xwsystem/utils/dt/contracts.py +2 -2
- exonware/xwsystem/utils/dt/defs.py +1 -1
- exonware/xwsystem/utils/dt/errors.py +1 -1
- exonware/xwsystem/utils/dt/formatting.py +3 -3
- exonware/xwsystem/utils/dt/humanize.py +1 -1
- exonware/xwsystem/utils/dt/parsing.py +2 -2
- exonware/xwsystem/utils/dt/timezone_utils.py +5 -5
- exonware/xwsystem/utils/errors.py +1 -1
- exonware/xwsystem/utils/test_runner.py +6 -6
- exonware/xwsystem/utils/utils_contracts.py +1 -1
- exonware/xwsystem/validation/__init__.py +1 -1
- exonware/xwsystem/validation/base.py +39 -39
- exonware/xwsystem/validation/contracts.py +8 -8
- exonware/xwsystem/validation/declarative.py +9 -9
- exonware/xwsystem/validation/defs.py +1 -1
- exonware/xwsystem/validation/errors.py +1 -1
- exonware/xwsystem/validation/fluent_validator.py +8 -8
- exonware/xwsystem/version.py +2 -2
- {exonware_xwsystem-0.0.1.408.dist-info → exonware_xwsystem-0.0.1.410.dist-info}/METADATA +9 -11
- exonware_xwsystem-0.0.1.410.dist-info/RECORD +273 -0
- {exonware_xwsystem-0.0.1.408.dist-info → exonware_xwsystem-0.0.1.410.dist-info}/WHEEL +1 -1
- exonware/xwsystem/lazy_bootstrap.py +0 -79
- exonware_xwsystem-0.0.1.408.dist-info/RECORD +0 -274
- {exonware_xwsystem-0.0.1.408.dist-info → exonware_xwsystem-0.0.1.410.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Company: eXonware.com
|
|
3
3
|
Author: Eng. Muhammad AlShehri
|
|
4
4
|
Email: connect@exonware.com
|
|
5
|
-
Version: 0.0.1.
|
|
5
|
+
Version: 0.0.1.410
|
|
6
6
|
Generation Date: November 2, 2025
|
|
7
7
|
|
|
8
8
|
XML serialization - Extensible Markup Language.
|
|
@@ -11,6 +11,12 @@ Following I→A pattern:
|
|
|
11
11
|
- I: ISerialization (interface)
|
|
12
12
|
- A: ASerialization (abstract base)
|
|
13
13
|
- Concrete: XmlSerializer
|
|
14
|
+
|
|
15
|
+
Improved implementation:
|
|
16
|
+
- Uses xmltodict for both encoding and decoding (better round-trip compatibility)
|
|
17
|
+
- Requires xmltodict >= 0.13.0 for security features
|
|
18
|
+
- Preserves types using XML attributes
|
|
19
|
+
- Minimal try/catch blocks with proper error handling
|
|
14
20
|
"""
|
|
15
21
|
|
|
16
22
|
from typing import Any, Optional, Union
|
|
@@ -21,20 +27,18 @@ from ....contracts import EncodeOptions, DecodeOptions
|
|
|
21
27
|
from ....defs import CodecCapability
|
|
22
28
|
from ....errors import SerializationError
|
|
23
29
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
30
|
+
# Primary: xmltodict for both encoding and decoding (better round-trip)
|
|
31
|
+
# xmltodict has built-in security features (disable_entities=True by default)
|
|
32
|
+
# No need for defusedxml - xmltodict handles XML security internally
|
|
33
|
+
import xmltodict
|
|
34
|
+
|
|
35
|
+
# Optional: dicttoxml as fallback (not recommended for round-trip)
|
|
27
36
|
try:
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
defuse_stdlib()
|
|
37
|
+
import dicttoxml
|
|
38
|
+
DICTTOXML_AVAILABLE = True
|
|
31
39
|
except ImportError:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# Lazy import for dicttoxml and xmltodict - the lazy hook will automatically handle ImportError
|
|
36
|
-
import dicttoxml
|
|
37
|
-
import xmltodict
|
|
40
|
+
DICTTOXML_AVAILABLE = False
|
|
41
|
+
dicttoxml = None
|
|
38
42
|
|
|
39
43
|
|
|
40
44
|
class XmlSerializer(ASerialization):
|
|
@@ -45,7 +49,8 @@ class XmlSerializer(ASerialization):
|
|
|
45
49
|
A: ASerialization (abstract base)
|
|
46
50
|
Concrete: XmlSerializer
|
|
47
51
|
|
|
48
|
-
Uses
|
|
52
|
+
Uses xmltodict for both encoding and decoding to ensure perfect round-trip compatibility.
|
|
53
|
+
Requires xmltodict >= 0.13.0 for security features.
|
|
49
54
|
|
|
50
55
|
Examples:
|
|
51
56
|
>>> serializer = XmlSerializer()
|
|
@@ -53,8 +58,9 @@ class XmlSerializer(ASerialization):
|
|
|
53
58
|
>>> # Encode data
|
|
54
59
|
>>> xml_str = serializer.encode({"user": {"name": "John", "age": 30}})
|
|
55
60
|
>>>
|
|
56
|
-
>>> # Decode data
|
|
57
|
-
>>> data = serializer.decode(
|
|
61
|
+
>>> # Decode data (perfect round-trip)
|
|
62
|
+
>>> data = serializer.decode(xml_str)
|
|
63
|
+
>>> assert data == {"user": {"name": "John", "age": 30}}
|
|
58
64
|
>>>
|
|
59
65
|
>>> # Save to file
|
|
60
66
|
>>> serializer.save_file({"config": {"debug": True}}, "config.xml")
|
|
@@ -66,10 +72,25 @@ class XmlSerializer(ASerialization):
|
|
|
66
72
|
def __init__(self):
|
|
67
73
|
"""Initialize XML serializer."""
|
|
68
74
|
super().__init__()
|
|
69
|
-
if
|
|
75
|
+
if xmltodict is None:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"xmltodict >= 0.13.0 is required for XML serialization. "
|
|
78
|
+
"Install with: pip install xmltodict>=0.13.0"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Verify security features are available
|
|
82
|
+
# Root cause fixed: Check for security features at initialization.
|
|
83
|
+
# Priority #1: Security - Use available security features, recommend upgrade for full security.
|
|
84
|
+
import inspect
|
|
85
|
+
parse_sig = inspect.signature(xmltodict.parse)
|
|
86
|
+
self._has_disable_entities = 'disable_entities' in parse_sig.parameters
|
|
87
|
+
self._has_forbid_dtd = 'forbid_dtd' in parse_sig.parameters
|
|
88
|
+
self._has_forbid_entities = 'forbid_entities' in parse_sig.parameters
|
|
89
|
+
|
|
90
|
+
if not self._has_disable_entities:
|
|
70
91
|
raise ImportError(
|
|
71
|
-
"
|
|
72
|
-
"Install with: pip install
|
|
92
|
+
"xmltodict with disable_entities support is required for XML serialization. "
|
|
93
|
+
"Install with: pip install 'xmltodict>=0.12.0'"
|
|
73
94
|
)
|
|
74
95
|
|
|
75
96
|
# ========================================================================
|
|
@@ -118,18 +139,212 @@ class XmlSerializer(ASerialization):
|
|
|
118
139
|
return ["serialization", "markup"]
|
|
119
140
|
|
|
120
141
|
# ========================================================================
|
|
121
|
-
#
|
|
142
|
+
# XML SANITIZATION HELPERS
|
|
143
|
+
# ========================================================================
|
|
144
|
+
|
|
145
|
+
def _sanitize_xml_name(self, name: str) -> str:
|
|
146
|
+
"""
|
|
147
|
+
Sanitize a string to be a valid XML element/attribute name.
|
|
148
|
+
|
|
149
|
+
XML 1.0 element name rules:
|
|
150
|
+
- Must start with letter, underscore, or colon (colon for namespaces)
|
|
151
|
+
- Can contain letters, digits, hyphens, underscores, periods, colons
|
|
152
|
+
- Cannot start with "xml" (case-insensitive)
|
|
153
|
+
- Cannot contain spaces or other special characters
|
|
154
|
+
|
|
155
|
+
Root cause fixed: Dictionary keys used as XML element names must be valid XML names.
|
|
156
|
+
Solution: Prefix invalid names with underscore, replace invalid chars with underscore.
|
|
157
|
+
Priority #2: Usability - Ensure all data can be serialized to XML.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
name: String to sanitize as XML name
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Valid XML element/attribute name
|
|
164
|
+
"""
|
|
165
|
+
import re
|
|
166
|
+
|
|
167
|
+
# Convert to string if not already
|
|
168
|
+
name_str = str(name)
|
|
169
|
+
|
|
170
|
+
# Replace invalid characters with underscore
|
|
171
|
+
# Valid chars: letters, digits, hyphens, underscores, periods, colons
|
|
172
|
+
sanitized = re.sub(r'[^a-zA-Z0-9_\-.:]', '_', name_str)
|
|
173
|
+
|
|
174
|
+
# If starts with digit, hyphen, period, or colon, prefix with underscore
|
|
175
|
+
if sanitized and sanitized[0] in '0123456789-.:':
|
|
176
|
+
sanitized = '_' + sanitized
|
|
177
|
+
|
|
178
|
+
# If starts with "xml" (case-insensitive), prefix with underscore
|
|
179
|
+
if sanitized.lower().startswith('xml'):
|
|
180
|
+
sanitized = '_' + sanitized
|
|
181
|
+
|
|
182
|
+
# If empty after sanitization, use default name
|
|
183
|
+
if not sanitized:
|
|
184
|
+
sanitized = '_item'
|
|
185
|
+
|
|
186
|
+
return sanitized
|
|
187
|
+
|
|
188
|
+
def _sanitize_for_xml(self, data: Any, preserve_keys: bool = True) -> Any:
|
|
189
|
+
"""
|
|
190
|
+
Sanitize data for XML encoding by removing/replacing invalid XML characters.
|
|
191
|
+
|
|
192
|
+
XML 1.0 doesn't allow certain control characters (0x00-0x1F except 0x09, 0x0A, 0x0D).
|
|
193
|
+
Also sanitizes dictionary keys to be valid XML element names.
|
|
194
|
+
|
|
195
|
+
Root cause fixed: Dictionary keys must be valid XML element names (can't start with digits).
|
|
196
|
+
Solution: Sanitize keys and store original keys as XML attributes for round-trip preservation.
|
|
197
|
+
Priority #2: Usability - Ensure all data structures can be serialized to XML with key preservation.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
data: Python data structure
|
|
201
|
+
preserve_keys: If True, store original keys as @_original_key attributes
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Sanitized data structure safe for XML encoding
|
|
205
|
+
"""
|
|
206
|
+
if isinstance(data, dict):
|
|
207
|
+
# Root cause fixed: Dictionary keys must be valid XML element names.
|
|
208
|
+
# Keys that start with digits (like UUIDs) are invalid XML element names.
|
|
209
|
+
# Solution: Sanitize keys and preserve originals as attributes for round-trip.
|
|
210
|
+
sanitized_dict = {}
|
|
211
|
+
for key, value in data.items():
|
|
212
|
+
# Sanitize key to be valid XML element name
|
|
213
|
+
sanitized_key = self._sanitize_xml_name(key)
|
|
214
|
+
# Handle key collisions (if sanitization produces duplicate keys)
|
|
215
|
+
original_key = sanitized_key
|
|
216
|
+
counter = 1
|
|
217
|
+
while sanitized_key in sanitized_dict:
|
|
218
|
+
sanitized_key = f"{original_key}_{counter}"
|
|
219
|
+
counter += 1
|
|
220
|
+
|
|
221
|
+
# Sanitize value recursively
|
|
222
|
+
sanitized_value = self._sanitize_for_xml(value, preserve_keys=preserve_keys)
|
|
223
|
+
|
|
224
|
+
# If key was changed and we want to preserve it, store original as attribute
|
|
225
|
+
if preserve_keys and sanitized_key != str(key):
|
|
226
|
+
# Wrap value in dict with original key as attribute
|
|
227
|
+
# xmltodict uses @ prefix for attributes
|
|
228
|
+
if isinstance(sanitized_value, dict):
|
|
229
|
+
# Add original key as attribute to existing dict
|
|
230
|
+
sanitized_value['@_original_key'] = str(key)
|
|
231
|
+
sanitized_dict[sanitized_key] = sanitized_value
|
|
232
|
+
else:
|
|
233
|
+
# Wrap non-dict value to add attribute
|
|
234
|
+
sanitized_dict[sanitized_key] = {
|
|
235
|
+
'@_original_key': str(key),
|
|
236
|
+
'#text': sanitized_value
|
|
237
|
+
}
|
|
238
|
+
else:
|
|
239
|
+
sanitized_dict[sanitized_key] = sanitized_value
|
|
240
|
+
return sanitized_dict
|
|
241
|
+
elif isinstance(data, list):
|
|
242
|
+
return [self._sanitize_for_xml(item, preserve_keys=preserve_keys) for item in data]
|
|
243
|
+
elif isinstance(data, str):
|
|
244
|
+
# Remove invalid XML 1.0 control characters (except tab, newline, carriage return)
|
|
245
|
+
# XML 1.0 allows: #x9 (tab), #xA (newline), #xD (carriage return)
|
|
246
|
+
# All other control chars (0x00-0x1F) are invalid
|
|
247
|
+
import re
|
|
248
|
+
# Remove control characters except tab, newline, carriage return
|
|
249
|
+
sanitized = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', data)
|
|
250
|
+
return sanitized
|
|
251
|
+
elif isinstance(data, (int, float, bool, type(None))):
|
|
252
|
+
return data
|
|
253
|
+
else:
|
|
254
|
+
# Convert other types to string and sanitize
|
|
255
|
+
return self._sanitize_for_xml(str(data))
|
|
256
|
+
|
|
257
|
+
# ========================================================================
|
|
258
|
+
# TYPE PRESERVATION HELPERS
|
|
259
|
+
# ========================================================================
|
|
260
|
+
|
|
261
|
+
def _preserve_types(self, data: Any) -> Any:
|
|
262
|
+
"""
|
|
263
|
+
Preserve Python types in XML structure using type hints.
|
|
264
|
+
|
|
265
|
+
Adds '@type' attributes to preserve type information for round-trip.
|
|
266
|
+
This allows us to restore int, float, bool, None from string representations.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
data: Python data structure
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Data structure with type hints embedded
|
|
273
|
+
"""
|
|
274
|
+
if isinstance(data, dict):
|
|
275
|
+
result = {}
|
|
276
|
+
for key, value in data.items():
|
|
277
|
+
if isinstance(value, (int, float, bool, type(None))):
|
|
278
|
+
# Store type information
|
|
279
|
+
result[key] = {
|
|
280
|
+
'@type': type(value).__name__,
|
|
281
|
+
'#text': str(value) if value is not None else ''
|
|
282
|
+
}
|
|
283
|
+
elif isinstance(value, dict):
|
|
284
|
+
result[key] = self._preserve_types(value)
|
|
285
|
+
elif isinstance(value, list):
|
|
286
|
+
result[key] = [self._preserve_types(item) for item in value]
|
|
287
|
+
else:
|
|
288
|
+
result[key] = value
|
|
289
|
+
return result
|
|
290
|
+
elif isinstance(data, list):
|
|
291
|
+
return [self._preserve_types(item) for item in data]
|
|
292
|
+
else:
|
|
293
|
+
return data
|
|
294
|
+
|
|
295
|
+
def _restore_types(self, data: Any) -> Any:
|
|
296
|
+
"""
|
|
297
|
+
Restore Python types from XML structure with type hints.
|
|
298
|
+
|
|
299
|
+
Converts '@type' attributes back to proper Python types.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
data: XML data structure with type hints
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Data structure with restored types
|
|
306
|
+
"""
|
|
307
|
+
if isinstance(data, dict):
|
|
308
|
+
# Check if this is a type-hinted value
|
|
309
|
+
if '@type' in data and '#text' in data and len(data) == 2:
|
|
310
|
+
type_name = data['@type']
|
|
311
|
+
text_value = data['#text']
|
|
312
|
+
|
|
313
|
+
if type_name == 'int':
|
|
314
|
+
return int(text_value) if text_value else 0
|
|
315
|
+
elif type_name == 'float':
|
|
316
|
+
return float(text_value) if text_value else 0.0
|
|
317
|
+
elif type_name == 'bool':
|
|
318
|
+
return text_value.lower() in ('true', '1', 'yes')
|
|
319
|
+
elif type_name == 'NoneType':
|
|
320
|
+
return None
|
|
321
|
+
else:
|
|
322
|
+
return text_value
|
|
323
|
+
|
|
324
|
+
# Recursively process dict
|
|
325
|
+
result = {}
|
|
326
|
+
for key, value in data.items():
|
|
327
|
+
if key not in ('@type', '#text'):
|
|
328
|
+
result[key] = self._restore_types(value)
|
|
329
|
+
return result
|
|
330
|
+
elif isinstance(data, list):
|
|
331
|
+
return [self._restore_types(item) for item in data]
|
|
332
|
+
else:
|
|
333
|
+
return data
|
|
334
|
+
|
|
335
|
+
# ========================================================================
|
|
336
|
+
# CORE ENCODE/DECODE (Using xmltodict for both - perfect round-trip)
|
|
122
337
|
# ========================================================================
|
|
123
338
|
|
|
124
339
|
def encode(self, value: Any, *, options: Optional[EncodeOptions] = None) -> Union[bytes, str]:
|
|
125
340
|
"""
|
|
126
341
|
Encode data to XML string.
|
|
127
342
|
|
|
128
|
-
Uses
|
|
343
|
+
Uses xmltodict.unparse() for encoding (better round-trip compatibility than dicttoxml).
|
|
129
344
|
|
|
130
345
|
Args:
|
|
131
346
|
value: Data to serialize
|
|
132
|
-
options: XML options (root,
|
|
347
|
+
options: XML options (root, pretty, preserve_types, etc.)
|
|
133
348
|
|
|
134
349
|
Returns:
|
|
135
350
|
XML string
|
|
@@ -137,44 +352,70 @@ class XmlSerializer(ASerialization):
|
|
|
137
352
|
Raises:
|
|
138
353
|
SerializationError: If encoding fails
|
|
139
354
|
"""
|
|
355
|
+
opts = options or {}
|
|
356
|
+
|
|
357
|
+
# Determine root element name
|
|
358
|
+
root_name = opts.get('root', 'root')
|
|
359
|
+
|
|
360
|
+
# Root cause fixed: Sanitize data to remove invalid XML characters.
|
|
361
|
+
# Priority #1: Security - Prevent XML injection and malformed XML.
|
|
362
|
+
# Priority #2: Usability - Ensure data can be encoded without errors.
|
|
363
|
+
value = self._sanitize_for_xml(value)
|
|
364
|
+
|
|
365
|
+
# Root cause fixed: Type preservation disabled by default - XML is text-based.
|
|
366
|
+
# Priority #2: Usability - Focus on structure preservation first, types are secondary.
|
|
367
|
+
# Note: Numbers will be strings in XML (this is expected XML behavior).
|
|
368
|
+
preserve_types = opts.get('preserve_types', False)
|
|
369
|
+
if preserve_types:
|
|
370
|
+
value = self._preserve_types(value)
|
|
371
|
+
|
|
372
|
+
# Wrap in root element if needed (xmltodict requires single root)
|
|
373
|
+
# Root cause fixed: Always wrap in root element for xmltodict compatibility.
|
|
374
|
+
if not isinstance(value, dict):
|
|
375
|
+
# Non-dict value - wrap it
|
|
376
|
+
wrapped_value = {root_name: value}
|
|
377
|
+
elif len(value) != 1:
|
|
378
|
+
# Multiple keys - wrap in root
|
|
379
|
+
wrapped_value = {root_name: value}
|
|
380
|
+
else:
|
|
381
|
+
# Single key dict - check if we should use it as root or wrap it
|
|
382
|
+
single_key = list(value.keys())[0]
|
|
383
|
+
if single_key == root_name:
|
|
384
|
+
# Already has correct root name
|
|
385
|
+
wrapped_value = value
|
|
386
|
+
else:
|
|
387
|
+
# Different root name - wrap it
|
|
388
|
+
wrapped_value = {root_name: value}
|
|
389
|
+
|
|
390
|
+
# Encode to XML string using xmltodict.unparse()
|
|
391
|
+
# Root cause fixed: Use xmltodict for both encode and decode for perfect round-trip.
|
|
392
|
+
# Priority #2: Usability - Round-trip serialization should preserve data structure.
|
|
140
393
|
try:
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
custom_root=opts.get('root', 'root'),
|
|
147
|
-
attr_type=opts.get('attr_type', False),
|
|
148
|
-
item_func=opts.get('item_func', lambda x: 'item')
|
|
394
|
+
xml_str = xmltodict.unparse(
|
|
395
|
+
wrapped_value,
|
|
396
|
+
pretty=opts.get('pretty', False),
|
|
397
|
+
indent=opts.get('indent', ' '),
|
|
398
|
+
full_document=opts.get('full_document', True)
|
|
149
399
|
)
|
|
150
|
-
|
|
151
|
-
# Convert to string
|
|
152
|
-
xml_str = xml_bytes.decode('utf-8')
|
|
153
|
-
|
|
154
|
-
# Pretty print if requested
|
|
155
|
-
if opts.get('pretty', False):
|
|
156
|
-
import xml.dom.minidom
|
|
157
|
-
dom = xml.dom.minidom.parseString(xml_bytes)
|
|
158
|
-
xml_str = dom.toprettyxml(indent=opts.get('indent', ' '))
|
|
159
|
-
|
|
160
|
-
return xml_str
|
|
161
|
-
|
|
162
|
-
except Exception as e:
|
|
400
|
+
except (ValueError, TypeError) as e:
|
|
163
401
|
raise SerializationError(
|
|
164
|
-
f"Failed to encode XML: {e}"
|
|
402
|
+
f"Failed to encode XML: {e}. "
|
|
403
|
+
f"Data may contain invalid XML characters or unsupported types.",
|
|
165
404
|
format_name=self.format_name,
|
|
166
405
|
original_error=e
|
|
167
|
-
)
|
|
406
|
+
) from e
|
|
407
|
+
|
|
408
|
+
return xml_str
|
|
168
409
|
|
|
169
410
|
def decode(self, repr: Union[bytes, str], *, options: Optional[DecodeOptions] = None) -> Any:
|
|
170
411
|
"""
|
|
171
412
|
Decode XML string to data.
|
|
172
413
|
|
|
173
|
-
Uses xmltodict.parse().
|
|
414
|
+
Uses xmltodict.parse() with security features enabled.
|
|
174
415
|
|
|
175
416
|
Args:
|
|
176
417
|
repr: XML string (bytes or str)
|
|
177
|
-
options: XML options (process_namespaces, etc.)
|
|
418
|
+
options: XML options (process_namespaces, root, preserve_types, etc.)
|
|
178
419
|
|
|
179
420
|
Returns:
|
|
180
421
|
Decoded Python dict
|
|
@@ -182,29 +423,163 @@ class XmlSerializer(ASerialization):
|
|
|
182
423
|
Raises:
|
|
183
424
|
SerializationError: If decoding fails
|
|
184
425
|
"""
|
|
426
|
+
# Convert bytes to str if needed
|
|
427
|
+
if isinstance(repr, bytes):
|
|
428
|
+
repr = repr.decode('utf-8')
|
|
429
|
+
|
|
430
|
+
opts = options or {}
|
|
431
|
+
root_name = opts.get('root', 'root')
|
|
432
|
+
preserve_types = opts.get('preserve_types', False)
|
|
433
|
+
|
|
434
|
+
# Decode from XML string with security features enabled
|
|
435
|
+
# Root cause fixed: Use available security features based on xmltodict version.
|
|
436
|
+
# Priority #1: Security - Use all available security features.
|
|
437
|
+
parse_kwargs = {
|
|
438
|
+
'process_namespaces': opts.get('process_namespaces', False),
|
|
439
|
+
'namespace_separator': opts.get('namespace_separator', ':'),
|
|
440
|
+
'disable_entities': True, # Security: disable external entities (available in >=0.12.0)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# Add additional security features if available (>=0.13.0)
|
|
444
|
+
if self._has_forbid_dtd:
|
|
445
|
+
parse_kwargs['forbid_dtd'] = True # Security: forbid DTD
|
|
446
|
+
if self._has_forbid_entities:
|
|
447
|
+
parse_kwargs['forbid_entities'] = True # Security: forbid entities
|
|
448
|
+
|
|
185
449
|
try:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
450
|
+
data = xmltodict.parse(repr, **parse_kwargs)
|
|
451
|
+
except Exception as e:
|
|
452
|
+
# Provide better error context for XML parsing failures
|
|
453
|
+
error_msg = str(e)
|
|
454
|
+
if "not well-formed" in error_msg or "ExpatError" in str(type(e).__name__):
|
|
455
|
+
# Try to find the problematic character position
|
|
456
|
+
raise SerializationError(
|
|
457
|
+
f"Failed to decode XML: {error_msg}. "
|
|
458
|
+
f"The XML may contain invalid characters or be malformed.",
|
|
459
|
+
format_name=self.format_name,
|
|
460
|
+
original_error=e
|
|
461
|
+
) from e
|
|
462
|
+
else:
|
|
463
|
+
raise SerializationError(
|
|
464
|
+
f"Failed to decode XML: {error_msg}",
|
|
465
|
+
format_name=self.format_name,
|
|
466
|
+
original_error=e
|
|
467
|
+
) from e
|
|
468
|
+
|
|
469
|
+
# Unwrap root element if it matches expected root name
|
|
470
|
+
# Root cause fixed: Proper root element handling - check if root matches expected name.
|
|
471
|
+
# Priority #2: Usability - Round-trip serialization should preserve data structure.
|
|
472
|
+
if isinstance(data, dict) and len(data) == 1:
|
|
473
|
+
# Check if the single key matches root_name or if it's a generic 'root'
|
|
474
|
+
keys = list(data.keys())
|
|
475
|
+
if keys[0] == root_name or (root_name == 'root' and keys[0] == 'root'):
|
|
476
|
+
data = data[keys[0]]
|
|
477
|
+
# If root doesn't match, keep wrapped (might be intentional)
|
|
478
|
+
|
|
479
|
+
# Restore original keys if they were preserved
|
|
480
|
+
# Root cause fixed: Dictionary keys were sanitized during encoding.
|
|
481
|
+
# Solution: Restore original keys from @_original_key attributes.
|
|
482
|
+
# Priority #2: Usability - Round-trip serialization must preserve key names.
|
|
483
|
+
data = self._restore_original_keys(data)
|
|
484
|
+
|
|
485
|
+
# Restore types if they were preserved
|
|
486
|
+
if preserve_types:
|
|
487
|
+
data = self._restore_types(data)
|
|
488
|
+
|
|
489
|
+
return data
|
|
490
|
+
|
|
491
|
+
def _infer_type(self, value: str) -> Any:
|
|
492
|
+
"""
|
|
493
|
+
Infer Python type from XML string value.
|
|
494
|
+
|
|
495
|
+
Root cause fixed: XML is text-based and converts all values to strings.
|
|
496
|
+
Solution: Attempt to infer and restore original types (int, float, bool, None).
|
|
497
|
+
Priority #2: Usability - Round-trip serialization should preserve types when possible.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
value: String value from XML
|
|
191
501
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
502
|
+
Returns:
|
|
503
|
+
Value with inferred type (int, float, bool, None, or original string)
|
|
504
|
+
"""
|
|
505
|
+
if not isinstance(value, str):
|
|
506
|
+
return value
|
|
507
|
+
|
|
508
|
+
value = value.strip()
|
|
509
|
+
|
|
510
|
+
# Check for None/empty
|
|
511
|
+
if not value or value.lower() in ('none', 'null', ''):
|
|
512
|
+
return None
|
|
513
|
+
|
|
514
|
+
# Check for boolean
|
|
515
|
+
if value.lower() in ('true', 'false'):
|
|
516
|
+
return value.lower() == 'true'
|
|
517
|
+
|
|
518
|
+
# Check for integer
|
|
519
|
+
try:
|
|
520
|
+
# Try int first (more common)
|
|
521
|
+
if value.isdigit() or (value.startswith('-') and value[1:].isdigit()):
|
|
522
|
+
return int(value)
|
|
523
|
+
except (ValueError, OverflowError):
|
|
524
|
+
pass
|
|
525
|
+
|
|
526
|
+
# Check for float
|
|
527
|
+
try:
|
|
528
|
+
return float(value)
|
|
529
|
+
except (ValueError, OverflowError):
|
|
530
|
+
pass
|
|
531
|
+
|
|
532
|
+
# Return original string if no type matches
|
|
533
|
+
return value
|
|
534
|
+
|
|
535
|
+
def _restore_original_keys(self, data: Any) -> Any:
|
|
536
|
+
"""
|
|
537
|
+
Restore original dictionary keys from @_original_key attributes.
|
|
538
|
+
|
|
539
|
+
Root cause fixed: Keys were sanitized during encoding (e.g., UUIDs starting with digits).
|
|
540
|
+
Solution: Check for @_original_key attributes and restore original key names.
|
|
541
|
+
Priority #2: Usability - Round-trip serialization must preserve key names.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
data: Decoded XML data structure
|
|
201
545
|
|
|
546
|
+
Returns:
|
|
547
|
+
Data structure with original keys restored and types inferred
|
|
548
|
+
"""
|
|
549
|
+
if isinstance(data, dict):
|
|
550
|
+
result = {}
|
|
551
|
+
for key, value in data.items():
|
|
552
|
+
# Skip xmltodict internal attributes
|
|
553
|
+
if key.startswith('@') and key != '@_original_key':
|
|
554
|
+
continue
|
|
555
|
+
|
|
556
|
+
# Check if this value has an original key attribute
|
|
557
|
+
if isinstance(value, dict) and '@_original_key' in value:
|
|
558
|
+
original_key = value['@_original_key']
|
|
559
|
+
# Remove the attribute from value
|
|
560
|
+
clean_value = {k: v for k, v in value.items() if k != '@_original_key'}
|
|
561
|
+
|
|
562
|
+
# If clean_value has only #text, unwrap it
|
|
563
|
+
if len(clean_value) == 1 and '#text' in clean_value:
|
|
564
|
+
clean_value = clean_value['#text']
|
|
565
|
+
# Infer type for unwrapped text value
|
|
566
|
+
clean_value = self._infer_type(clean_value)
|
|
567
|
+
|
|
568
|
+
# Recursively restore keys in the value
|
|
569
|
+
clean_value = self._restore_original_keys(clean_value)
|
|
570
|
+
result[original_key] = clean_value
|
|
571
|
+
else:
|
|
572
|
+
# Recursively restore keys in the value
|
|
573
|
+
restored_value = self._restore_original_keys(value)
|
|
574
|
+
# Infer type for leaf string values
|
|
575
|
+
if isinstance(restored_value, str):
|
|
576
|
+
restored_value = self._infer_type(restored_value)
|
|
577
|
+
result[key] = restored_value
|
|
578
|
+
return result
|
|
579
|
+
elif isinstance(data, list):
|
|
580
|
+
return [self._restore_original_keys(item) for item in data]
|
|
581
|
+
else:
|
|
582
|
+
# Infer type for leaf string values
|
|
583
|
+
if isinstance(data, str):
|
|
584
|
+
return self._infer_type(data)
|
|
202
585
|
return data
|
|
203
|
-
|
|
204
|
-
except (Exception, UnicodeDecodeError) as e:
|
|
205
|
-
raise SerializationError(
|
|
206
|
-
f"Failed to decode XML: {e}",
|
|
207
|
-
format_name=self.format_name,
|
|
208
|
-
original_error=e
|
|
209
|
-
)
|
|
210
|
-
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Company: eXonware.com
|
|
3
3
|
Author: Eng. Muhammad AlShehri
|
|
4
4
|
Email: connect@exonware.com
|
|
5
|
-
Version: 0.0.1.
|
|
5
|
+
Version: 0.0.1.410
|
|
6
6
|
Generation Date: November 2, 2025
|
|
7
7
|
|
|
8
8
|
Serialization Registry - Delegates to UniversalCodecRegistry.
|
|
@@ -10,7 +10,7 @@ Serialization Registry - Delegates to UniversalCodecRegistry.
|
|
|
10
10
|
Provides serialization-specific convenience methods for format discovery.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
-
from typing import Optional,
|
|
13
|
+
from typing import Optional, Union
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
|
|
16
16
|
from ..codec.registry import UniversalCodecRegistry, get_registry
|
|
@@ -117,7 +117,7 @@ class SerializationRegistry:
|
|
|
117
117
|
"""
|
|
118
118
|
return self._codec_registry.get_by_mime_type(mime_type)
|
|
119
119
|
|
|
120
|
-
def list_formats(self) ->
|
|
120
|
+
def list_formats(self) -> list[str]:
|
|
121
121
|
"""
|
|
122
122
|
List all registered format IDs.
|
|
123
123
|
|
|
@@ -130,7 +130,7 @@ class SerializationRegistry:
|
|
|
130
130
|
"""
|
|
131
131
|
return self._codec_registry.list_codecs()
|
|
132
132
|
|
|
133
|
-
def list_extensions(self) ->
|
|
133
|
+
def list_extensions(self) -> list[str]:
|
|
134
134
|
"""
|
|
135
135
|
List all registered file extensions.
|
|
136
136
|
|
|
@@ -143,7 +143,7 @@ class SerializationRegistry:
|
|
|
143
143
|
"""
|
|
144
144
|
return self._codec_registry.list_extensions()
|
|
145
145
|
|
|
146
|
-
def list_mime_types(self) ->
|
|
146
|
+
def list_mime_types(self) -> list[str]:
|
|
147
147
|
"""
|
|
148
148
|
List all registered MIME types.
|
|
149
149
|
|