smithy-xml 0.2.0__tar.gz → 0.3.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.
- smithy_xml-0.3.0/.changes/0.3.0.json +16 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/.gitignore +3 -0
- smithy_xml-0.3.0/CHANGELOG.md +25 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/PKG-INFO +8 -1
- smithy_xml-0.3.0/README.md +10 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/pyproject.toml +1 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/src/smithy_xml/__init__.py +21 -9
- smithy_xml-0.3.0/src/smithy_xml/_private/deserializers.py +298 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/src/smithy_xml/settings.py +2 -0
- smithy_xml-0.2.0/CHANGELOG.md +0 -14
- smithy_xml-0.2.0/README.md +0 -4
- smithy_xml-0.2.0/src/smithy_xml/_private/deserializers.py +0 -365
- smithy_xml-0.2.0/src/smithy_xml/_private/readers.py +0 -55
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/.changes/0.1.0.json +0 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/.changes/0.2.0.json +0 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/NOTICE +0 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/src/smithy_xml/_private/__init__.py +0 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/src/smithy_xml/_private/serializers.py +0 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/src/smithy_xml/_private/traits.py +0 -0
- {smithy_xml-0.2.0 → smithy_xml-0.3.0}/src/smithy_xml/py.typed +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"changes": [
|
|
3
|
+
{
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"description": "Declared explicit `__all__` exports on all public modules. Names that were previously importable only incidentally (via `from module import *` or as un-annotated module attributes) are no longer part of the public API; import public symbols by name from their documented module."
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
"type": "feature",
|
|
9
|
+
"description": "XML payloads are now eagerly parsed before deserialization, improving performance for both `bytes` and `BytesReader` inputs."
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"type": "enhancement",
|
|
13
|
+
"description": "Added support for Python 3.15."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## v0.3.0
|
|
4
|
+
|
|
5
|
+
### Breaking Changes
|
|
6
|
+
* Declared explicit `__all__` exports on all public modules. Names that were previously importable only incidentally (via `from module import *` or as un-annotated module attributes) are no longer part of the public API; import public symbols by name from their documented module.
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
* XML payloads are now eagerly parsed before deserialization, improving performance for both `bytes` and `BytesReader` inputs.
|
|
10
|
+
|
|
11
|
+
### Enhancements
|
|
12
|
+
* Added support for Python 3.15.
|
|
13
|
+
|
|
14
|
+
## v0.2.0
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
* Added XML serialization support to `XMLCodec`.
|
|
18
|
+
|
|
19
|
+
### Bug fixes
|
|
20
|
+
* Fixed deserialization of `@xmlFlattened` structure lists and of prefixed `@xmlAttribute` names. The root element name is no longer validated, and a structure's `@xmlName` no longer renames members that target it.
|
|
21
|
+
|
|
22
|
+
## v0.1.0
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
* Added initial support for XML deserialization in Smithy clients.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: smithy-xml
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: XML serialization and deserialization support for Smithy tooling.
|
|
5
5
|
Project-URL: Changelog, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-xml/CHANGELOG.md
|
|
6
6
|
Project-URL: Code, https://github.com/smithy-lang/smithy-python/blob/develop/packages/smithy-xml/
|
|
@@ -21,6 +21,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
22
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
23
|
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
25
|
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
25
26
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
26
27
|
Classifier: Topic :: Software Development :: Libraries
|
|
@@ -32,3 +33,9 @@ Description-Content-Type: text/markdown
|
|
|
32
33
|
|
|
33
34
|
This package provides generic XML serialization and deserialization support
|
|
34
35
|
for Smithy clients and servers.
|
|
36
|
+
|
|
37
|
+
## Deserialization
|
|
38
|
+
|
|
39
|
+
`XMLCodec` eagerly parses `bytes` and `BytesReader` payloads before
|
|
40
|
+
deserializing Smithy shapes. This improves deserialization performance while
|
|
41
|
+
temporarily materializing the complete XML tree.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# smithy-xml
|
|
2
|
+
|
|
3
|
+
This package provides generic XML serialization and deserialization support
|
|
4
|
+
for Smithy clients and servers.
|
|
5
|
+
|
|
6
|
+
## Deserialization
|
|
7
|
+
|
|
8
|
+
`XMLCodec` eagerly parses `bytes` and `BytesReader` payloads before
|
|
9
|
+
deserializing Smithy shapes. This improves deserialization performance while
|
|
10
|
+
temporarily materializing the complete XML tree.
|
|
@@ -22,6 +22,7 @@ classifiers = [
|
|
|
22
22
|
"Programming Language :: Python :: 3.12",
|
|
23
23
|
"Programming Language :: Python :: 3.13",
|
|
24
24
|
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: 3.15",
|
|
25
26
|
"Programming Language :: Python :: Implementation :: CPython",
|
|
26
27
|
"Programming Language :: Python :: Free Threading :: 2 - Beta",
|
|
27
28
|
"Topic :: Software Development :: Libraries"
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
|
|
4
|
-
from io import BytesIO
|
|
5
4
|
from typing import Any
|
|
6
|
-
from xml.etree.ElementTree import
|
|
5
|
+
from xml.etree.ElementTree import Element, fromstring, parse
|
|
7
6
|
|
|
8
7
|
from smithy_core.codecs import Codec
|
|
9
8
|
from smithy_core.deserializers import ShapeDeserializer
|
|
@@ -13,11 +12,10 @@ from smithy_core.shapes import ShapeID
|
|
|
13
12
|
from smithy_core.types import TimestampFormat
|
|
14
13
|
|
|
15
14
|
from ._private.deserializers import XMLShapeDeserializer as _XMLShapeDeserializer
|
|
16
|
-
from ._private.readers import XMLEventReader as _XMLEventReader
|
|
17
15
|
from ._private.serializers import XMLShapeSerializer as _XMLShapeSerializer
|
|
18
16
|
from .settings import XMLSettings
|
|
19
17
|
|
|
20
|
-
__version__ = "0.
|
|
18
|
+
__version__ = "0.3.0"
|
|
21
19
|
__all__ = ("XMLCodec", "XMLSettings")
|
|
22
20
|
|
|
23
21
|
|
|
@@ -60,11 +58,25 @@ class XMLCodec(Codec):
|
|
|
60
58
|
*,
|
|
61
59
|
wrapper_elements: tuple[str, ...] = (),
|
|
62
60
|
) -> ShapeDeserializer:
|
|
63
|
-
|
|
64
|
-
source
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
root = (
|
|
62
|
+
fromstring(source) # noqa: S314
|
|
63
|
+
if isinstance(source, bytes)
|
|
64
|
+
else parse(source).getroot() # noqa: S314
|
|
67
65
|
)
|
|
66
|
+
return self.create_deserializer_from_element(
|
|
67
|
+
root,
|
|
68
|
+
wrapper_elements=wrapper_elements,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def create_deserializer_from_element(
|
|
72
|
+
self,
|
|
73
|
+
root: Element,
|
|
74
|
+
*,
|
|
75
|
+
wrapper_elements: tuple[str, ...] = (),
|
|
76
|
+
) -> ShapeDeserializer:
|
|
77
|
+
"""Create a deserializer from an already parsed XML root element."""
|
|
68
78
|
return _XMLShapeDeserializer(
|
|
69
|
-
settings=self._settings,
|
|
79
|
+
settings=self._settings,
|
|
80
|
+
root=root,
|
|
81
|
+
wrapper_elements=wrapper_elements,
|
|
70
82
|
)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
from base64 import b64decode
|
|
6
|
+
from collections.abc import Callable, Iterable
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
from xml.etree.ElementTree import Element
|
|
9
|
+
|
|
10
|
+
from smithy_core.deserializers import ShapeDeserializer, SpecificShapeDeserializer
|
|
11
|
+
from smithy_core.documents import Document
|
|
12
|
+
from smithy_core.exceptions import SmithyError
|
|
13
|
+
from smithy_core.schemas import Schema
|
|
14
|
+
from smithy_core.shapes import ShapeID
|
|
15
|
+
from smithy_core.traits import (
|
|
16
|
+
TimestampFormatTrait,
|
|
17
|
+
XMLAttributeTrait,
|
|
18
|
+
XMLFlattenedTrait,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from ..settings import XMLSettings
|
|
22
|
+
from .traits import member_xml_name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _local_name(tag: str) -> str:
|
|
26
|
+
"""Strip namespace URI from an element tag: {uri}local -> local."""
|
|
27
|
+
if tag.startswith("{"):
|
|
28
|
+
return tag.split("}", 1)[1]
|
|
29
|
+
return tag
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _local_attr_name(name: str) -> str:
|
|
33
|
+
"""Strip a namespace prefix from a modeled attribute name: prefix:local -> local."""
|
|
34
|
+
return name.rpartition(":")[2]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _validate_element_name(expected: str, elem: Element) -> None:
|
|
38
|
+
"""Raise XMLParseError if the element's local name doesn't match expected."""
|
|
39
|
+
found = _local_name(elem.tag)
|
|
40
|
+
if found != expected:
|
|
41
|
+
raise XMLParseError(f"Expected element '{expected}', got '{found}'")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_xml_float(text: str) -> float:
|
|
45
|
+
"""Parse an XML float string, handling NaN and Infinity."""
|
|
46
|
+
match text:
|
|
47
|
+
case "NaN":
|
|
48
|
+
return float("nan")
|
|
49
|
+
case "Infinity":
|
|
50
|
+
return float("inf")
|
|
51
|
+
case "-Infinity":
|
|
52
|
+
return float("-inf")
|
|
53
|
+
case _:
|
|
54
|
+
return float(text)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class XMLParseError(SmithyError):
|
|
58
|
+
def __init__(self, message: str) -> None:
|
|
59
|
+
super().__init__(f"Error parsing XML: {message}")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class XMLShapeDeserializer(ShapeDeserializer):
|
|
63
|
+
"""Deserializer backed by a fully materialized ElementTree value."""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
settings: XMLSettings,
|
|
68
|
+
root: Element,
|
|
69
|
+
wrapper_elements: tuple[str, ...] = (),
|
|
70
|
+
*,
|
|
71
|
+
elements: tuple[Element, ...] | None = None,
|
|
72
|
+
xml_names: dict[ShapeID, dict[str, Schema]] | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
self._settings = settings
|
|
75
|
+
self._xml_names = xml_names if xml_names is not None else {}
|
|
76
|
+
self._elements = elements
|
|
77
|
+
self._from_wrapper = bool(wrapper_elements)
|
|
78
|
+
|
|
79
|
+
current = root
|
|
80
|
+
for index, wrapper in enumerate(wrapper_elements):
|
|
81
|
+
_validate_element_name(wrapper, current)
|
|
82
|
+
if index < len(wrapper_elements) - 1:
|
|
83
|
+
child = next(iter(current), None)
|
|
84
|
+
if child is None:
|
|
85
|
+
raise XMLParseError(
|
|
86
|
+
f"Expected wrapper element '{wrapper_elements[index + 1]}'"
|
|
87
|
+
)
|
|
88
|
+
current = child
|
|
89
|
+
self._element = current
|
|
90
|
+
|
|
91
|
+
def is_null(self) -> bool:
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
def read_null(self) -> None:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def read_boolean(self, schema: Schema) -> bool:
|
|
98
|
+
text = self._read_text()
|
|
99
|
+
match text:
|
|
100
|
+
case "true":
|
|
101
|
+
return True
|
|
102
|
+
case "false":
|
|
103
|
+
return False
|
|
104
|
+
case _:
|
|
105
|
+
raise XMLParseError(f"Expected 'true' or 'false', got '{text}'")
|
|
106
|
+
|
|
107
|
+
def read_blob(self, schema: Schema) -> bytes:
|
|
108
|
+
return b64decode(self._read_text())
|
|
109
|
+
|
|
110
|
+
def read_integer(self, schema: Schema) -> int:
|
|
111
|
+
return int(self._read_text())
|
|
112
|
+
|
|
113
|
+
def read_float(self, schema: Schema) -> float:
|
|
114
|
+
return _parse_xml_float(self._read_text())
|
|
115
|
+
|
|
116
|
+
def read_big_decimal(self, schema: Schema) -> Decimal:
|
|
117
|
+
return Decimal(self._read_text())
|
|
118
|
+
|
|
119
|
+
def read_string(self, schema: Schema) -> str:
|
|
120
|
+
return self._read_text()
|
|
121
|
+
|
|
122
|
+
def read_document(self, schema: Schema) -> Document:
|
|
123
|
+
raise NotImplementedError("XML does not support document types")
|
|
124
|
+
|
|
125
|
+
def read_timestamp(self, schema: Schema) -> datetime.datetime:
|
|
126
|
+
format = self._settings.default_timestamp_format
|
|
127
|
+
if self._settings.use_timestamp_format:
|
|
128
|
+
if format_trait := schema.get_trait(TimestampFormatTrait):
|
|
129
|
+
format = format_trait.format
|
|
130
|
+
return format.deserialize(self._read_text())
|
|
131
|
+
|
|
132
|
+
def read_struct(
|
|
133
|
+
self,
|
|
134
|
+
schema: Schema,
|
|
135
|
+
consumer: Callable[[Schema, ShapeDeserializer], None],
|
|
136
|
+
) -> None:
|
|
137
|
+
element = self._element
|
|
138
|
+
xml_names = self._get_xml_names(schema)
|
|
139
|
+
|
|
140
|
+
if not self._from_wrapper:
|
|
141
|
+
for member_schema in schema.members.values():
|
|
142
|
+
if member_schema.get_trait(XMLAttributeTrait) is None:
|
|
143
|
+
continue
|
|
144
|
+
expected_name = _local_attr_name(member_xml_name(member_schema))
|
|
145
|
+
for attribute_name, value in element.attrib.items():
|
|
146
|
+
if _local_name(attribute_name) == expected_name:
|
|
147
|
+
consumer(
|
|
148
|
+
member_schema,
|
|
149
|
+
_AttributeDeserializer(value, self._settings),
|
|
150
|
+
)
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
flattened: dict[str, list[Element]] = {}
|
|
154
|
+
for child in element:
|
|
155
|
+
name = _local_name(child.tag)
|
|
156
|
+
member_schema = xml_names.get(name)
|
|
157
|
+
if member_schema is None:
|
|
158
|
+
continue
|
|
159
|
+
if member_schema.get_trait(XMLFlattenedTrait) is not None:
|
|
160
|
+
flattened.setdefault(name, []).append(child)
|
|
161
|
+
else:
|
|
162
|
+
consumer(member_schema, self._for_element(child))
|
|
163
|
+
|
|
164
|
+
for name, elements in flattened.items():
|
|
165
|
+
consumer(
|
|
166
|
+
xml_names[name],
|
|
167
|
+
self._for_elements(elements),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def read_list(
|
|
171
|
+
self,
|
|
172
|
+
schema: Schema,
|
|
173
|
+
consumer: Callable[[ShapeDeserializer], None],
|
|
174
|
+
) -> None:
|
|
175
|
+
elements: Iterable[Element]
|
|
176
|
+
if schema.get_trait(XMLFlattenedTrait) is not None:
|
|
177
|
+
elements = self._elements or (self._element,)
|
|
178
|
+
else:
|
|
179
|
+
elements = self._element
|
|
180
|
+
|
|
181
|
+
for element in elements:
|
|
182
|
+
consumer(self._for_element(element))
|
|
183
|
+
|
|
184
|
+
def read_map(
|
|
185
|
+
self,
|
|
186
|
+
schema: Schema,
|
|
187
|
+
consumer: Callable[[str, ShapeDeserializer], None],
|
|
188
|
+
) -> None:
|
|
189
|
+
key_tag = member_xml_name(schema.members["key"])
|
|
190
|
+
value_tag = member_xml_name(schema.members["value"])
|
|
191
|
+
entries: Iterable[Element]
|
|
192
|
+
if schema.get_trait(XMLFlattenedTrait) is not None:
|
|
193
|
+
entries = self._elements or (self._element,)
|
|
194
|
+
else:
|
|
195
|
+
entries = self._element
|
|
196
|
+
|
|
197
|
+
for entry in entries:
|
|
198
|
+
self._read_map_entry(entry, key_tag, value_tag, consumer)
|
|
199
|
+
|
|
200
|
+
def _read_map_entry(
|
|
201
|
+
self,
|
|
202
|
+
entry: Element,
|
|
203
|
+
key_tag: str,
|
|
204
|
+
value_tag: str,
|
|
205
|
+
consumer: Callable[[str, ShapeDeserializer], None],
|
|
206
|
+
) -> None:
|
|
207
|
+
key: str | None = None
|
|
208
|
+
for child in entry:
|
|
209
|
+
name = _local_name(child.tag)
|
|
210
|
+
if name == key_tag:
|
|
211
|
+
key = child.text or ""
|
|
212
|
+
elif name == value_tag:
|
|
213
|
+
if key is None:
|
|
214
|
+
raise XMLParseError(
|
|
215
|
+
"Map key element must appear before value element"
|
|
216
|
+
)
|
|
217
|
+
consumer(key, self._for_element(child))
|
|
218
|
+
|
|
219
|
+
def _read_text(self) -> str:
|
|
220
|
+
return self._element.text or ""
|
|
221
|
+
|
|
222
|
+
def _for_element(self, element: Element) -> "XMLShapeDeserializer":
|
|
223
|
+
return XMLShapeDeserializer(
|
|
224
|
+
settings=self._settings,
|
|
225
|
+
root=element,
|
|
226
|
+
xml_names=self._xml_names,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _for_elements(self, elements: list[Element]) -> "XMLShapeDeserializer":
|
|
230
|
+
return XMLShapeDeserializer(
|
|
231
|
+
settings=self._settings,
|
|
232
|
+
root=elements[0],
|
|
233
|
+
elements=tuple(elements),
|
|
234
|
+
xml_names=self._xml_names,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def _get_xml_names(self, schema: Schema) -> dict[str, Schema]:
|
|
238
|
+
if schema.id in self._xml_names:
|
|
239
|
+
return self._xml_names[schema.id]
|
|
240
|
+
result = {
|
|
241
|
+
member_xml_name(member_schema): member_schema
|
|
242
|
+
for member_schema in schema.members.values()
|
|
243
|
+
if member_schema.get_trait(XMLAttributeTrait) is None
|
|
244
|
+
}
|
|
245
|
+
self._xml_names[schema.id] = result
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _AttributeDeserializer(SpecificShapeDeserializer):
|
|
250
|
+
"""Deserializer for a value extracted from an XML attribute string."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, value: str, settings: XMLSettings) -> None:
|
|
253
|
+
self._value = value
|
|
254
|
+
self._settings = settings
|
|
255
|
+
|
|
256
|
+
def read_string(self, schema: Schema) -> str:
|
|
257
|
+
return self._value
|
|
258
|
+
|
|
259
|
+
def read_boolean(self, schema: Schema) -> bool:
|
|
260
|
+
match self._value:
|
|
261
|
+
case "true":
|
|
262
|
+
return True
|
|
263
|
+
case "false":
|
|
264
|
+
return False
|
|
265
|
+
case _:
|
|
266
|
+
raise XMLParseError(f"Expected 'true' or 'false', got '{self._value}'")
|
|
267
|
+
|
|
268
|
+
def read_byte(self, schema: Schema) -> int:
|
|
269
|
+
return self.read_integer(schema)
|
|
270
|
+
|
|
271
|
+
def read_short(self, schema: Schema) -> int:
|
|
272
|
+
return self.read_integer(schema)
|
|
273
|
+
|
|
274
|
+
def read_integer(self, schema: Schema) -> int:
|
|
275
|
+
return int(self._value)
|
|
276
|
+
|
|
277
|
+
def read_long(self, schema: Schema) -> int:
|
|
278
|
+
return self.read_integer(schema)
|
|
279
|
+
|
|
280
|
+
def read_big_integer(self, schema: Schema) -> int:
|
|
281
|
+
return self.read_integer(schema)
|
|
282
|
+
|
|
283
|
+
def read_float(self, schema: Schema) -> float:
|
|
284
|
+
return _parse_xml_float(self._value)
|
|
285
|
+
|
|
286
|
+
def read_double(self, schema: Schema) -> float:
|
|
287
|
+
return self.read_float(schema)
|
|
288
|
+
|
|
289
|
+
def read_big_decimal(self, schema: Schema) -> Decimal:
|
|
290
|
+
return Decimal(self._value)
|
|
291
|
+
|
|
292
|
+
def read_timestamp(self, schema: Schema) -> datetime.datetime:
|
|
293
|
+
format = self._settings.default_timestamp_format
|
|
294
|
+
if self._settings.use_timestamp_format:
|
|
295
|
+
if format_trait := schema.get_trait(TimestampFormatTrait):
|
|
296
|
+
format = format_trait.format
|
|
297
|
+
|
|
298
|
+
return format.deserialize(self._value)
|
smithy_xml-0.2.0/CHANGELOG.md
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## v0.2.0
|
|
4
|
-
|
|
5
|
-
### Features
|
|
6
|
-
* Added XML serialization support to `XMLCodec`.
|
|
7
|
-
|
|
8
|
-
### Bug fixes
|
|
9
|
-
* Fixed deserialization of `@xmlFlattened` structure lists and of prefixed `@xmlAttribute` names. The root element name is no longer validated, and a structure's `@xmlName` no longer renames members that target it.
|
|
10
|
-
|
|
11
|
-
## v0.1.0
|
|
12
|
-
|
|
13
|
-
### Features
|
|
14
|
-
* Added initial support for XML deserialization in Smithy clients.
|
smithy_xml-0.2.0/README.md
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
import datetime
|
|
5
|
-
from base64 import b64decode
|
|
6
|
-
from collections.abc import Callable
|
|
7
|
-
from decimal import Decimal
|
|
8
|
-
from xml.etree.ElementTree import Element
|
|
9
|
-
|
|
10
|
-
from smithy_core.deserializers import ShapeDeserializer, SpecificShapeDeserializer
|
|
11
|
-
from smithy_core.documents import Document
|
|
12
|
-
from smithy_core.exceptions import SmithyError
|
|
13
|
-
from smithy_core.schemas import Schema
|
|
14
|
-
from smithy_core.shapes import ShapeID
|
|
15
|
-
from smithy_core.traits import (
|
|
16
|
-
TimestampFormatTrait,
|
|
17
|
-
XMLAttributeTrait,
|
|
18
|
-
XMLFlattenedTrait,
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
from ..settings import XMLSettings
|
|
22
|
-
from .readers import XMLEvent, XMLEventReader
|
|
23
|
-
from .traits import member_xml_name
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def _local_name(tag: str) -> str:
|
|
27
|
-
"""Strip namespace URI from an element tag: {uri}local -> local."""
|
|
28
|
-
if tag.startswith("{"):
|
|
29
|
-
return tag.split("}", 1)[1]
|
|
30
|
-
return tag
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _local_attr_name(name: str) -> str:
|
|
34
|
-
"""Strip a namespace prefix from a modeled attribute name: prefix:local -> local."""
|
|
35
|
-
return name.rpartition(":")[2]
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _validate_element_name(expected: str, elem: Element) -> None:
|
|
39
|
-
"""Raise XMLParseError if the element's local name doesn't match expected."""
|
|
40
|
-
found = _local_name(elem.tag)
|
|
41
|
-
if found != expected:
|
|
42
|
-
raise XMLParseError(f"Expected element '{expected}', got '{found}'")
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _parse_xml_float(text: str) -> float:
|
|
46
|
-
"""Parse an XML float string, handling NaN and Infinity."""
|
|
47
|
-
match text:
|
|
48
|
-
case "NaN":
|
|
49
|
-
return float("nan")
|
|
50
|
-
case "Infinity":
|
|
51
|
-
return float("inf")
|
|
52
|
-
case "-Infinity":
|
|
53
|
-
return float("-inf")
|
|
54
|
-
case _:
|
|
55
|
-
return float(text)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class XMLParseError(SmithyError):
|
|
59
|
-
def __init__(self, message: str) -> None:
|
|
60
|
-
super().__init__(f"Error parsing XML: {message}")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class XMLShapeDeserializer(ShapeDeserializer):
|
|
64
|
-
"""Deserializer that reads XML from a streaming pull parser."""
|
|
65
|
-
|
|
66
|
-
def __init__(
|
|
67
|
-
self,
|
|
68
|
-
settings: XMLSettings,
|
|
69
|
-
reader: XMLEventReader,
|
|
70
|
-
wrapper_elements: tuple[str, ...] = (),
|
|
71
|
-
) -> None:
|
|
72
|
-
self._settings = settings
|
|
73
|
-
self._reader = reader
|
|
74
|
-
self._xml_names: dict[ShapeID, dict[str, Schema]] = {}
|
|
75
|
-
self._preconsumed_start: Element | None = None
|
|
76
|
-
|
|
77
|
-
# Wrapper elements are protocol transport containers (e.g. awsQuery's
|
|
78
|
-
# <OpResponse><OpResult>). The last wrapper's start element is kept
|
|
79
|
-
# so that the next read can reuse it.
|
|
80
|
-
for wrapper in wrapper_elements:
|
|
81
|
-
event = next(self._reader)
|
|
82
|
-
if event.type != "start":
|
|
83
|
-
raise XMLParseError(f"Expected start element, got '{event.type}'")
|
|
84
|
-
_validate_element_name(wrapper, event.elem)
|
|
85
|
-
self._preconsumed_start = event.elem
|
|
86
|
-
|
|
87
|
-
def is_null(self) -> bool:
|
|
88
|
-
return False
|
|
89
|
-
|
|
90
|
-
def read_null(self) -> None:
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
def read_boolean(self, schema: Schema) -> bool:
|
|
94
|
-
text = self._read_text()
|
|
95
|
-
match text:
|
|
96
|
-
case "true":
|
|
97
|
-
return True
|
|
98
|
-
case "false":
|
|
99
|
-
return False
|
|
100
|
-
case _:
|
|
101
|
-
raise XMLParseError(f"Expected 'true' or 'false', got '{text}'")
|
|
102
|
-
|
|
103
|
-
def read_blob(self, schema: Schema) -> bytes:
|
|
104
|
-
return b64decode(self._read_text())
|
|
105
|
-
|
|
106
|
-
def read_integer(self, schema: Schema) -> int:
|
|
107
|
-
return int(self._read_text())
|
|
108
|
-
|
|
109
|
-
def read_float(self, schema: Schema) -> float:
|
|
110
|
-
return _parse_xml_float(self._read_text())
|
|
111
|
-
|
|
112
|
-
def read_big_decimal(self, schema: Schema) -> Decimal:
|
|
113
|
-
return Decimal(self._read_text())
|
|
114
|
-
|
|
115
|
-
def read_string(self, schema: Schema) -> str:
|
|
116
|
-
return self._read_text()
|
|
117
|
-
|
|
118
|
-
def read_document(self, schema: Schema) -> Document:
|
|
119
|
-
raise NotImplementedError("XML does not support document types")
|
|
120
|
-
|
|
121
|
-
def read_timestamp(self, schema: Schema) -> datetime.datetime:
|
|
122
|
-
format = self._settings.default_timestamp_format
|
|
123
|
-
if self._settings.use_timestamp_format:
|
|
124
|
-
if format_trait := schema.get_trait(TimestampFormatTrait):
|
|
125
|
-
format = format_trait.format
|
|
126
|
-
|
|
127
|
-
text = self._read_text()
|
|
128
|
-
return format.deserialize(text)
|
|
129
|
-
|
|
130
|
-
def read_struct(
|
|
131
|
-
self,
|
|
132
|
-
schema: Schema,
|
|
133
|
-
consumer: Callable[[Schema, "ShapeDeserializer"], None],
|
|
134
|
-
) -> None:
|
|
135
|
-
xml_names = self._get_xml_names(schema)
|
|
136
|
-
start_from_wrapper = self._preconsumed_start is not None
|
|
137
|
-
start_elem = self._consume_start_event()
|
|
138
|
-
|
|
139
|
-
# Wrapper elements are protocol transport containers, not modeled structs,
|
|
140
|
-
# so their attributes cannot be deserialized as struct members.
|
|
141
|
-
if not start_from_wrapper:
|
|
142
|
-
for member_schema in schema.members.values():
|
|
143
|
-
if member_schema.get_trait(XMLAttributeTrait) is None:
|
|
144
|
-
continue
|
|
145
|
-
# Prefixed names (e.g. ``xsi:name``) are parsed into a
|
|
146
|
-
# namespace-qualified key, so only the local parts are compared.
|
|
147
|
-
expected_attr_name = _local_attr_name(member_xml_name(member_schema))
|
|
148
|
-
for attr_name, attr_value in start_elem.attrib.items():
|
|
149
|
-
attr_local_name = _local_name(attr_name)
|
|
150
|
-
if attr_local_name == expected_attr_name:
|
|
151
|
-
consumer(
|
|
152
|
-
member_schema,
|
|
153
|
-
_AttributeDeserializer(attr_value, self._settings),
|
|
154
|
-
)
|
|
155
|
-
break
|
|
156
|
-
|
|
157
|
-
# Flattened members lack an enclosing element, so there is no way to
|
|
158
|
-
# know when all items have been parsed. Their events are collected
|
|
159
|
-
# during iteration and replayed through a bounded reader afterwards.
|
|
160
|
-
flattened_buffers: dict[str, list[XMLEvent]] = {}
|
|
161
|
-
flattened_names = {
|
|
162
|
-
xml_name: member_schema
|
|
163
|
-
for xml_name, member_schema in xml_names.items()
|
|
164
|
-
if member_schema.get_trait(XMLFlattenedTrait) is not None
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
while self._reader.peek().type != "end":
|
|
168
|
-
tag = _local_name(self._reader.peek().elem.tag)
|
|
169
|
-
|
|
170
|
-
if tag in flattened_names:
|
|
171
|
-
flattened_buffers.setdefault(tag, []).extend(self._buffer_element())
|
|
172
|
-
elif tag in xml_names:
|
|
173
|
-
consumer(xml_names[tag], self)
|
|
174
|
-
else:
|
|
175
|
-
# Skip unknown tag
|
|
176
|
-
self._consume_start_event()
|
|
177
|
-
self._skip_to_end()
|
|
178
|
-
|
|
179
|
-
next(self._reader)
|
|
180
|
-
|
|
181
|
-
for tag, events in flattened_buffers.items():
|
|
182
|
-
member_schema = flattened_names[tag]
|
|
183
|
-
buffered_de = XMLShapeDeserializer(
|
|
184
|
-
self._settings,
|
|
185
|
-
XMLEventReader(iter(events)),
|
|
186
|
-
)
|
|
187
|
-
consumer(member_schema, buffered_de)
|
|
188
|
-
|
|
189
|
-
def read_list(
|
|
190
|
-
self,
|
|
191
|
-
schema: Schema,
|
|
192
|
-
consumer: Callable[["ShapeDeserializer"], None],
|
|
193
|
-
) -> None:
|
|
194
|
-
is_flattened = schema.get_trait(XMLFlattenedTrait) is not None
|
|
195
|
-
if not is_flattened:
|
|
196
|
-
self._consume_start_event()
|
|
197
|
-
while self._reader.peek().type != "end":
|
|
198
|
-
consumer(self)
|
|
199
|
-
else:
|
|
200
|
-
while self._reader.has_next():
|
|
201
|
-
consumer(self)
|
|
202
|
-
|
|
203
|
-
if not is_flattened:
|
|
204
|
-
next(self._reader)
|
|
205
|
-
|
|
206
|
-
def read_map(
|
|
207
|
-
self,
|
|
208
|
-
schema: Schema,
|
|
209
|
-
consumer: Callable[[str, "ShapeDeserializer"], None],
|
|
210
|
-
) -> None:
|
|
211
|
-
is_flattened = schema.get_trait(XMLFlattenedTrait) is not None
|
|
212
|
-
key_schema = schema.members["key"]
|
|
213
|
-
value_schema = schema.members["value"]
|
|
214
|
-
key_tag = member_xml_name(key_schema)
|
|
215
|
-
value_tag = member_xml_name(value_schema)
|
|
216
|
-
|
|
217
|
-
if not is_flattened:
|
|
218
|
-
self._consume_start_event()
|
|
219
|
-
while self._reader.peek().type != "end":
|
|
220
|
-
self._read_map_entry(key_tag, value_tag, consumer)
|
|
221
|
-
else:
|
|
222
|
-
while self._reader.has_next():
|
|
223
|
-
self._read_map_entry(key_tag, value_tag, consumer)
|
|
224
|
-
|
|
225
|
-
if not is_flattened:
|
|
226
|
-
next(self._reader)
|
|
227
|
-
|
|
228
|
-
def _read_text(self) -> str:
|
|
229
|
-
"""Consume a complete element (start through end) and return its text."""
|
|
230
|
-
elem = self._consume_start_event()
|
|
231
|
-
self._skip_to_end()
|
|
232
|
-
# elem.text is populated only after consuming the "end" event
|
|
233
|
-
return elem.text or ""
|
|
234
|
-
|
|
235
|
-
def _consume_start_event(self) -> Element:
|
|
236
|
-
"""Consume and return the next start element.
|
|
237
|
-
|
|
238
|
-
If a start element was pre-consumed (e.g. from consuming wrapper elements),
|
|
239
|
-
it is returned first and cleared.
|
|
240
|
-
"""
|
|
241
|
-
if self._preconsumed_start is not None:
|
|
242
|
-
elem = self._preconsumed_start
|
|
243
|
-
self._preconsumed_start = None
|
|
244
|
-
return elem
|
|
245
|
-
event = next(self._reader)
|
|
246
|
-
if event.type != "start":
|
|
247
|
-
raise XMLParseError(f"Expected start element, got '{event.type}'")
|
|
248
|
-
return event.elem
|
|
249
|
-
|
|
250
|
-
def _skip_to_end(self) -> None:
|
|
251
|
-
"""Skip to the matching end event. Assumes start was already consumed."""
|
|
252
|
-
depth = 1
|
|
253
|
-
while depth > 0:
|
|
254
|
-
event = next(self._reader)
|
|
255
|
-
if event.type == "start":
|
|
256
|
-
depth += 1
|
|
257
|
-
elif event.type == "end":
|
|
258
|
-
depth -= 1
|
|
259
|
-
|
|
260
|
-
def _buffer_element(self) -> list[XMLEvent]:
|
|
261
|
-
"""Buffer a complete element's events (start through matching end)."""
|
|
262
|
-
events: list[XMLEvent] = []
|
|
263
|
-
event = next(self._reader)
|
|
264
|
-
events.append(event)
|
|
265
|
-
depth = 1
|
|
266
|
-
while depth > 0:
|
|
267
|
-
event = next(self._reader)
|
|
268
|
-
events.append(event)
|
|
269
|
-
if event.type == "start":
|
|
270
|
-
depth += 1
|
|
271
|
-
elif event.type == "end":
|
|
272
|
-
depth -= 1
|
|
273
|
-
return events
|
|
274
|
-
|
|
275
|
-
def _get_xml_names(self, schema: Schema) -> dict[str, Schema]:
|
|
276
|
-
"""Get or build the XML element name -> member schema mapping for a shape."""
|
|
277
|
-
if schema.id in self._xml_names:
|
|
278
|
-
return self._xml_names[schema.id]
|
|
279
|
-
result: dict[str, Schema] = {}
|
|
280
|
-
for member_schema in schema.members.values():
|
|
281
|
-
if member_schema.get_trait(XMLAttributeTrait) is not None:
|
|
282
|
-
continue
|
|
283
|
-
xml_name = member_xml_name(member_schema)
|
|
284
|
-
result[xml_name] = member_schema
|
|
285
|
-
self._xml_names[schema.id] = result
|
|
286
|
-
return result
|
|
287
|
-
|
|
288
|
-
def _read_map_entry(
|
|
289
|
-
self,
|
|
290
|
-
key_tag: str,
|
|
291
|
-
value_tag: str,
|
|
292
|
-
consumer: Callable[[str, "ShapeDeserializer"], None],
|
|
293
|
-
) -> None:
|
|
294
|
-
"""Read one map entry element and emit key/value pairs via consumer."""
|
|
295
|
-
self._consume_start_event()
|
|
296
|
-
|
|
297
|
-
key: str | None = None
|
|
298
|
-
while self._reader.peek().type != "end":
|
|
299
|
-
child_tag = _local_name(self._reader.peek().elem.tag)
|
|
300
|
-
if child_tag == key_tag:
|
|
301
|
-
key = self._read_text()
|
|
302
|
-
elif child_tag == value_tag:
|
|
303
|
-
if key is None:
|
|
304
|
-
raise XMLParseError(
|
|
305
|
-
"Map key element must appear before value element"
|
|
306
|
-
)
|
|
307
|
-
consumer(key, self)
|
|
308
|
-
else:
|
|
309
|
-
# Skip unknown child tag
|
|
310
|
-
self._consume_start_event()
|
|
311
|
-
self._skip_to_end()
|
|
312
|
-
|
|
313
|
-
next(self._reader)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
class _AttributeDeserializer(SpecificShapeDeserializer):
|
|
317
|
-
"""Deserializer for a value extracted from an XML attribute string."""
|
|
318
|
-
|
|
319
|
-
def __init__(self, value: str, settings: XMLSettings) -> None:
|
|
320
|
-
self._value = value
|
|
321
|
-
self._settings = settings
|
|
322
|
-
|
|
323
|
-
def read_string(self, schema: Schema) -> str:
|
|
324
|
-
return self._value
|
|
325
|
-
|
|
326
|
-
def read_boolean(self, schema: Schema) -> bool:
|
|
327
|
-
match self._value:
|
|
328
|
-
case "true":
|
|
329
|
-
return True
|
|
330
|
-
case "false":
|
|
331
|
-
return False
|
|
332
|
-
case _:
|
|
333
|
-
raise XMLParseError(f"Expected 'true' or 'false', got '{self._value}'")
|
|
334
|
-
|
|
335
|
-
def read_byte(self, schema: Schema) -> int:
|
|
336
|
-
return self.read_integer(schema)
|
|
337
|
-
|
|
338
|
-
def read_short(self, schema: Schema) -> int:
|
|
339
|
-
return self.read_integer(schema)
|
|
340
|
-
|
|
341
|
-
def read_integer(self, schema: Schema) -> int:
|
|
342
|
-
return int(self._value)
|
|
343
|
-
|
|
344
|
-
def read_long(self, schema: Schema) -> int:
|
|
345
|
-
return self.read_integer(schema)
|
|
346
|
-
|
|
347
|
-
def read_big_integer(self, schema: Schema) -> int:
|
|
348
|
-
return self.read_integer(schema)
|
|
349
|
-
|
|
350
|
-
def read_float(self, schema: Schema) -> float:
|
|
351
|
-
return _parse_xml_float(self._value)
|
|
352
|
-
|
|
353
|
-
def read_double(self, schema: Schema) -> float:
|
|
354
|
-
return self.read_float(schema)
|
|
355
|
-
|
|
356
|
-
def read_big_decimal(self, schema: Schema) -> Decimal:
|
|
357
|
-
return Decimal(self._value)
|
|
358
|
-
|
|
359
|
-
def read_timestamp(self, schema: Schema) -> datetime.datetime:
|
|
360
|
-
format = self._settings.default_timestamp_format
|
|
361
|
-
if self._settings.use_timestamp_format:
|
|
362
|
-
if format_trait := schema.get_trait(TimestampFormatTrait):
|
|
363
|
-
format = format_trait.format
|
|
364
|
-
|
|
365
|
-
return format.deserialize(self._value)
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
|
|
4
|
-
from collections.abc import Iterator
|
|
5
|
-
from typing import NamedTuple
|
|
6
|
-
from xml.etree.ElementTree import Element
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class XMLEvent(NamedTuple):
|
|
10
|
-
type: str
|
|
11
|
-
elem: Element
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class XMLEventReader:
|
|
15
|
-
"""Buffered iterator over XML pull parser events with peek support.
|
|
16
|
-
|
|
17
|
-
Wraps an iterator of ``(event, element)`` tuples — either from
|
|
18
|
-
``iterparse`` (streaming from a byte source) or from an in-memory list
|
|
19
|
-
(for flattened member replay).
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
def __init__(self, events: Iterator[tuple[str, Element] | XMLEvent]) -> None:
|
|
23
|
-
self._iter = events
|
|
24
|
-
self._pending: XMLEvent | None = None
|
|
25
|
-
|
|
26
|
-
def __iter__(self):
|
|
27
|
-
return self
|
|
28
|
-
|
|
29
|
-
def __next__(self) -> XMLEvent:
|
|
30
|
-
if self._pending is not None:
|
|
31
|
-
result = self._pending
|
|
32
|
-
self._pending = None
|
|
33
|
-
return result
|
|
34
|
-
return self._next()
|
|
35
|
-
|
|
36
|
-
def _next(self) -> XMLEvent:
|
|
37
|
-
event = next(self._iter)
|
|
38
|
-
if isinstance(event, XMLEvent):
|
|
39
|
-
return event
|
|
40
|
-
event_type, elem = event
|
|
41
|
-
return XMLEvent(event_type, elem)
|
|
42
|
-
|
|
43
|
-
def has_next(self) -> bool:
|
|
44
|
-
if self._pending is not None:
|
|
45
|
-
return True
|
|
46
|
-
try:
|
|
47
|
-
self._pending = self._next()
|
|
48
|
-
return True
|
|
49
|
-
except StopIteration:
|
|
50
|
-
return False
|
|
51
|
-
|
|
52
|
-
def peek(self) -> XMLEvent:
|
|
53
|
-
if self._pending is None:
|
|
54
|
-
self._pending = self._next()
|
|
55
|
-
return self._pending
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|