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.
@@ -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
+ }
@@ -34,3 +34,6 @@ htmlcov
34
34
  *.egg-info
35
35
  dist
36
36
  .venv
37
+
38
+ # make gen
39
+ /workspace/
@@ -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.2.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 iterparse
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.2.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
- if isinstance(source, bytes):
64
- source = BytesIO(source)
65
- reader = _XMLEventReader(
66
- iterparse(source, events=("start", "end")) # noqa: S314
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, reader=reader, wrapper_elements=wrapper_elements
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)
@@ -4,6 +4,8 @@ from dataclasses import dataclass
4
4
 
5
5
  from smithy_core.types import TimestampFormat
6
6
 
7
+ __all__ = ["XMLSettings"]
8
+
7
9
 
8
10
  @dataclass(slots=True)
9
11
  class XMLSettings:
@@ -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.
@@ -1,4 +0,0 @@
1
- # smithy-xml
2
-
3
- This package provides generic XML serialization and deserialization support
4
- for Smithy clients and servers.
@@ -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