mms-client 1.0.5__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.
- mms_client/__init__.py +0 -0
- mms_client/client.py +14 -0
- mms_client/py.typed +0 -0
- mms_client/schemas/wsdl/mi-web-service-jbms.wsdl +276 -0
- mms_client/schemas/wsdl/omi-web-service.wsdl +262 -0
- mms_client/schemas/xsd/mi-market.xsd +2395 -0
- mms_client/schemas/xsd/mi-outbnd-reports.xsd +1489 -0
- mms_client/schemas/xsd/mi-report.xsd +379 -0
- mms_client/schemas/xsd/mpr.xsd +1817 -0
- mms_client/schemas/xsd/omi.xsd +793 -0
- mms_client/security/__init__.py +0 -0
- mms_client/security/certs.py +44 -0
- mms_client/security/crypto.py +57 -0
- mms_client/services/__init__.py +0 -0
- mms_client/services/base.py +591 -0
- mms_client/services/market.py +107 -0
- mms_client/services/omi.py +13 -0
- mms_client/services/registration.py +13 -0
- mms_client/services/report.py +13 -0
- mms_client/types/__init__.py +0 -0
- mms_client/types/base.py +272 -0
- mms_client/types/enums.py +18 -0
- mms_client/types/fields.py +153 -0
- mms_client/types/market.py +61 -0
- mms_client/types/offer.py +163 -0
- mms_client/types/transport.py +130 -0
- mms_client/utils/__init__.py +0 -0
- mms_client/utils/errors.py +66 -0
- mms_client/utils/serialization.py +513 -0
- mms_client/utils/web.py +220 -0
- mms_client-1.0.5.dist-info/LICENSE +24 -0
- mms_client-1.0.5.dist-info/METADATA +202 -0
- mms_client-1.0.5.dist-info/RECORD +34 -0
- mms_client-1.0.5.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Contains objects for serialization and deserialization of MMS data."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Type
|
|
10
|
+
from typing import Union
|
|
11
|
+
from typing import get_args
|
|
12
|
+
from typing import get_origin
|
|
13
|
+
|
|
14
|
+
from lxml.etree import XMLSchema
|
|
15
|
+
from lxml.etree import _Element as Element
|
|
16
|
+
from lxml.etree import parse
|
|
17
|
+
from pydantic_xml import element
|
|
18
|
+
|
|
19
|
+
from mms_client.types.base import E
|
|
20
|
+
from mms_client.types.base import Messages
|
|
21
|
+
from mms_client.types.base import MultiResponse
|
|
22
|
+
from mms_client.types.base import P
|
|
23
|
+
from mms_client.types.base import Payload
|
|
24
|
+
from mms_client.types.base import PayloadBase
|
|
25
|
+
from mms_client.types.base import Response
|
|
26
|
+
from mms_client.types.base import ResponseCommon
|
|
27
|
+
from mms_client.types.base import ResponseData
|
|
28
|
+
from mms_client.types.base import SchemaType
|
|
29
|
+
|
|
30
|
+
# Directory containing all our XML schemas
|
|
31
|
+
XSD_DIR = Path(__file__).parent.parent / "schemas" / "xsd"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Serializer:
|
|
35
|
+
"""Contains methods for serializing and deserializing MMS data."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, xsd: SchemaType, payload_key: str):
|
|
38
|
+
"""Create a new payload configuration with the given XSD schema, payload key, and interface type.
|
|
39
|
+
|
|
40
|
+
Arguments:
|
|
41
|
+
xsd (SchemaType): The XSD schema to use for validation.
|
|
42
|
+
payload_key (str): The key to use for the payload in the request.
|
|
43
|
+
interface (InterfaceType): The type of interface to use for the request.
|
|
44
|
+
"""
|
|
45
|
+
# Save the configuration for later use
|
|
46
|
+
self._xsd = xsd
|
|
47
|
+
self._payload_key = payload_key
|
|
48
|
+
|
|
49
|
+
# Get a reference to the XSD file so we can use it for validation
|
|
50
|
+
with open(XSD_DIR / self._xsd.value, "rb") as f:
|
|
51
|
+
self._schema = XMLSchema(parse(f))
|
|
52
|
+
|
|
53
|
+
def serialize(self, request_envelope: E, request_data: P) -> bytes:
|
|
54
|
+
"""Serialize the envelope and data to a byte string for sending to the MMS server.
|
|
55
|
+
|
|
56
|
+
Arguments:
|
|
57
|
+
request_envelope (Envelope): The envelope to be serialized.
|
|
58
|
+
request_data (Payload): The data to be serialized.
|
|
59
|
+
|
|
60
|
+
Returns: A byte string containing the XML-formatted data to be sent to the MMS server.
|
|
61
|
+
"""
|
|
62
|
+
# First, create our payload class from the payload and data types
|
|
63
|
+
payload_cls = _create_request_payload_type(
|
|
64
|
+
self._payload_key,
|
|
65
|
+
type(request_envelope), # type: ignore[arg-type]
|
|
66
|
+
type(request_data), # type: ignore[arg-type]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Next, inject the payload and data into the payload class
|
|
70
|
+
# Note: this returns a type that inherits from PayloadBase and the arguments provided to the initializer
|
|
71
|
+
# here are correct, but mypy thinks they are incorrect because it doesn't understand the the inherited type
|
|
72
|
+
payload = payload_cls(request_envelope, request_data, self._xsd.value) # type: ignore[call-arg, misc]
|
|
73
|
+
|
|
74
|
+
# Finally, convert the payload to XML and return it
|
|
75
|
+
# Note: we provided the encoding here so this will return bytes, not a string
|
|
76
|
+
return payload.to_xml(skip_empty=True, encoding="utf-8", xml_declaration=True) # type: ignore[return-value]
|
|
77
|
+
|
|
78
|
+
def deserialize(self, data: bytes, envelope_type: Type[E], data_type: Type[P]) -> Response[E, P]:
|
|
79
|
+
"""Deserialize the data to a response object.
|
|
80
|
+
|
|
81
|
+
Arguments:
|
|
82
|
+
data (bytes): The raw data to be deserialized.
|
|
83
|
+
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
84
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
85
|
+
|
|
86
|
+
Returns: A response object containing the envelope and data extracted from the raw data.
|
|
87
|
+
"""
|
|
88
|
+
tree = self._from_xml(data)
|
|
89
|
+
return self._from_tree(tree, envelope_type, data_type)
|
|
90
|
+
|
|
91
|
+
def deserialize_multi(self, data: bytes, envelope_type: Type[E], data_type: Type[P]) -> MultiResponse[E, P]:
|
|
92
|
+
"""Deserialize the data to a multi-response object.
|
|
93
|
+
|
|
94
|
+
Arguments:
|
|
95
|
+
data (bytes): The raw data to be deserialized.
|
|
96
|
+
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
97
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
98
|
+
|
|
99
|
+
Returns: A multi-response object containing the envelope and data extracted from the raw data.
|
|
100
|
+
"""
|
|
101
|
+
tree = self._from_xml(data)
|
|
102
|
+
return self._from_tree_multi(tree, envelope_type, data_type)
|
|
103
|
+
|
|
104
|
+
def _from_tree(self, raw: Element, envelope_type: Type[E], data_type: Type[P]) -> Response[E, P]:
|
|
105
|
+
"""Convert the raw data to a response object.
|
|
106
|
+
|
|
107
|
+
Arguments:
|
|
108
|
+
raw (Element): The raw data to be converted.
|
|
109
|
+
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
110
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
111
|
+
|
|
112
|
+
Returns: A response object containing the envelope and data extracted from the raw data.
|
|
113
|
+
"""
|
|
114
|
+
# First, attempt to extract the response from the raw data; if the key isn't found then we'll raise an error.
|
|
115
|
+
# Otherwise, we'll attempt to construct the response from the raw data.
|
|
116
|
+
if self._payload_key != raw.tag:
|
|
117
|
+
raise ValueError(f"Expected payload key '{self._payload_key}' not found in response")
|
|
118
|
+
cls: Response[E, P] = _create_response_payload_type( # type: ignore[assignment]
|
|
119
|
+
self._payload_key,
|
|
120
|
+
envelope_type, # type: ignore[arg-type]
|
|
121
|
+
data_type, # type: ignore[arg-type]
|
|
122
|
+
False,
|
|
123
|
+
)
|
|
124
|
+
resp = cls.from_xml_tree(raw) # type: ignore[arg-type]
|
|
125
|
+
|
|
126
|
+
# Next, attempt to extract the envelope and data from within the response
|
|
127
|
+
resp.envelope, resp.envelope_validation, envelope_node = self._from_tree_envelope(raw, envelope_type)
|
|
128
|
+
|
|
129
|
+
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
130
|
+
# from within the envelope
|
|
131
|
+
self._verify_tree_data_tag(envelope_node, data_type)
|
|
132
|
+
resp.payload = self._from_tree_data(envelope_node.find(data_type.__name__), data_type)
|
|
133
|
+
|
|
134
|
+
# Finally, attempt to extract the messages from within the payload
|
|
135
|
+
resp.messages = self._from_tree_messages(raw, envelope_type, data_type, self._payload_key, False)
|
|
136
|
+
|
|
137
|
+
# Return the response
|
|
138
|
+
return resp
|
|
139
|
+
|
|
140
|
+
def _from_tree_multi(self, raw: Element, envelope_type: Type[E], data_type: Type[P]) -> MultiResponse[E, P]:
|
|
141
|
+
"""Convert the raw data to a multi-response object.
|
|
142
|
+
|
|
143
|
+
Arguments:
|
|
144
|
+
raw (Element): The raw data to be converted.
|
|
145
|
+
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
146
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
147
|
+
|
|
148
|
+
Returns: A multi-response object containing the envelope and data extracted from the raw data.
|
|
149
|
+
"""
|
|
150
|
+
# First, attempt to extract the response from the raw data; if the key isn't found then we'll raise an error.
|
|
151
|
+
# Otherwise, we'll attempt to construct the response from the raw data.
|
|
152
|
+
if self._payload_key != raw.tag:
|
|
153
|
+
raise ValueError(f"Expected payload key '{self._payload_key}' not found in response")
|
|
154
|
+
cls: MultiResponse[E, P] = _create_response_payload_type( # type: ignore[assignment]
|
|
155
|
+
self._payload_key,
|
|
156
|
+
envelope_type, # type: ignore[arg-type]
|
|
157
|
+
data_type, # type: ignore[arg-type]
|
|
158
|
+
True,
|
|
159
|
+
)
|
|
160
|
+
resp = cls.from_xml_tree(raw) # type: ignore[arg-type]
|
|
161
|
+
|
|
162
|
+
# Next, attempt to extract the envelope from the response
|
|
163
|
+
resp.envelope, resp.envelope_validation, env_node = self._from_tree_envelope(raw, envelope_type)
|
|
164
|
+
|
|
165
|
+
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
166
|
+
# from within the envelope
|
|
167
|
+
# Note: apparently, mypy doesn't know about setter-getter properties either...
|
|
168
|
+
self._verify_tree_data_tag(env_node, data_type)
|
|
169
|
+
resp.payload = [
|
|
170
|
+
self._from_tree_data(item, data_type) for item in env_node.findall(data_type.__name__) # type: ignore[misc]
|
|
171
|
+
]
|
|
172
|
+
|
|
173
|
+
# Finally, attempt to extract the messages from within the payload
|
|
174
|
+
resp.messages = self._from_tree_messages(raw, envelope_type, data_type, self._payload_key, True)
|
|
175
|
+
|
|
176
|
+
# Return the response
|
|
177
|
+
return resp
|
|
178
|
+
|
|
179
|
+
def _from_tree_envelope(self, raw: Element, envelope_type: Type[E]) -> Tuple[E, ResponseCommon, Element]:
|
|
180
|
+
"""Attempt to extract the envelope from within the response.
|
|
181
|
+
|
|
182
|
+
Arguments:
|
|
183
|
+
raw (Element): The raw data to be converted.
|
|
184
|
+
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Envelope: The request payload constructed from the raw data.
|
|
188
|
+
ResponseCommon: The validation information for the request payload.
|
|
189
|
+
"""
|
|
190
|
+
# First, attempt to extract the envelope from within the response; if the key isn't found then we'll raise an
|
|
191
|
+
# exception to indicate that the envelope wasn't found.
|
|
192
|
+
envelope_node = raw.find(envelope_type.__name__)
|
|
193
|
+
if envelope_node is None:
|
|
194
|
+
raise ValueError(f"Expected envelope type '{envelope_type.__name__}' not found in response")
|
|
195
|
+
|
|
196
|
+
# Next, create a new envelope type that contains the envelope type with the appropriate XML tag. We have to do
|
|
197
|
+
# this because the envelope type doesn't include the ResponseCommon fields, and the tag doesn't match
|
|
198
|
+
# "ResponseCommon", so parsing will fail if we try to separate the envelope and common fields.
|
|
199
|
+
common_cls = _create_response_common_type(envelope_type) # type: ignore[arg-type]
|
|
200
|
+
|
|
201
|
+
# Finally, parse and return the envelope data and validation information
|
|
202
|
+
return (
|
|
203
|
+
envelope_type.from_xml_tree(envelope_node), # type: ignore[arg-type]
|
|
204
|
+
common_cls.from_xml_tree(envelope_node), # type: ignore[arg-type]
|
|
205
|
+
envelope_node,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def _verify_tree_data_tag(self, raw: Element, data_type: Type[P]) -> None:
|
|
209
|
+
"""Verify that no types other than the expected data type are present in the response.
|
|
210
|
+
|
|
211
|
+
Arguments:
|
|
212
|
+
raw (Element): The raw data to be converted.
|
|
213
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ValueError: If the expected data type is not found in the response.
|
|
217
|
+
"""
|
|
218
|
+
data_tags = set(node.tag for node in raw)
|
|
219
|
+
if not data_tags.issubset([data_type.__name__, "Messages"]):
|
|
220
|
+
raise ValueError(f"Expected data type '{data_type.__name__}' not found in response")
|
|
221
|
+
|
|
222
|
+
def _from_tree_data(self, raw: Optional[Element], data_type: Type[P]) -> Optional[ResponseData[P]]:
|
|
223
|
+
"""Attempt to extract the data from within the payload.
|
|
224
|
+
|
|
225
|
+
Arguments:
|
|
226
|
+
raw (Element): The raw data to be converted.
|
|
227
|
+
data_type (Type[BaseModel]): The type of data to be constructed.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
BaseModel: The data constructed from the raw data.
|
|
231
|
+
ResponseCommon: The validation information for the data extracted from the response.
|
|
232
|
+
"""
|
|
233
|
+
# First, verify that the data type is present in the response; if it isn't then we'll raise an exception to
|
|
234
|
+
# indicate that the data wasn't found.
|
|
235
|
+
if raw is None:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
# Next, create a new data type that contains the data type with the appropriate XML tag. We have to do this
|
|
239
|
+
common_cls = _create_response_common_type(data_type) # type: ignore[arg-type]
|
|
240
|
+
|
|
241
|
+
# Finally, parse and return the data and validation information
|
|
242
|
+
return ResponseData[P](
|
|
243
|
+
data_type.from_xml_tree(raw), # type: ignore[arg-type]
|
|
244
|
+
common_cls.from_xml_tree(raw), # type: ignore[arg-type]
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _from_tree_messages(
|
|
248
|
+
self, raw: Element, envelope_type: Type[E], current_type: Type[P], root: str, multi: bool
|
|
249
|
+
) -> Dict[str, Messages]:
|
|
250
|
+
"""Attempt to extract the messages from within the payload.
|
|
251
|
+
|
|
252
|
+
Note that this method is called recursively to handle nested payloads and data types. It does not iterate over
|
|
253
|
+
the XML tree to determine the structure of the response; instead, it uses the type annotations to determine the
|
|
254
|
+
structure of the response.
|
|
255
|
+
|
|
256
|
+
Arguments:
|
|
257
|
+
raw (Element): The raw data to be converted.
|
|
258
|
+
envelope_type (Type[Envelope]): The type of envelope being constructed.
|
|
259
|
+
current_type (Type[Payload]): The type of data being constructed.
|
|
260
|
+
root (str): The root of the dictionary, used to create the key for the messages.
|
|
261
|
+
multi (bool): Whether we're processing a list of nodes or a single node. If called with the
|
|
262
|
+
payload root, this value will determine whether we're processing a multi-
|
|
263
|
+
response or a single response.
|
|
264
|
+
"""
|
|
265
|
+
# First, find the Messages node in the raw data
|
|
266
|
+
message_node = raw.find("Messages")
|
|
267
|
+
|
|
268
|
+
# Next, create our dictionary of messages and attempt to extract the messages from the raw data at the current
|
|
269
|
+
# level of the response and set it to the root key
|
|
270
|
+
messages = {}
|
|
271
|
+
if message_node is not None:
|
|
272
|
+
messages[root] = Messages.from_xml_tree(message_node) # type: ignore[arg-type]
|
|
273
|
+
|
|
274
|
+
# Next, we need to call this method recursively, depending on where we are in the response object. If we are
|
|
275
|
+
# at the root of the response, we need to call this method for the envelope type. If we are at the envelope
|
|
276
|
+
# type, we need to call this method for the data type. If we are at the data type, we need to call this method
|
|
277
|
+
# for each field in the data type that is a Payload type.
|
|
278
|
+
if root == self._payload_key:
|
|
279
|
+
messages.update(
|
|
280
|
+
self._from_tree_messages(
|
|
281
|
+
_find_or_fail(raw, envelope_type.__name__),
|
|
282
|
+
envelope_type,
|
|
283
|
+
current_type,
|
|
284
|
+
f"{root}.{envelope_type.__name__}",
|
|
285
|
+
multi,
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
elif root.endswith(envelope_type.__name__):
|
|
289
|
+
messages.update(
|
|
290
|
+
self._from_tree_messages_inner(raw, envelope_type, current_type, root, current_type.__name__, multi)
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
# Iterate over each field on the current type...
|
|
294
|
+
for name, field in current_type.model_fields.items():
|
|
295
|
+
print(f"Checking field {name} with type {field.annotation}...")
|
|
296
|
+
|
|
297
|
+
# First, get the arguments and origin of the field's annotation
|
|
298
|
+
args = get_args(field.annotation)
|
|
299
|
+
origin = get_origin(field.annotation)
|
|
300
|
+
has_args = len(args) > 0
|
|
301
|
+
|
|
302
|
+
# Next, check if the annotation is a subclass of Payload or else if it's a collection of Payload. If
|
|
303
|
+
# neither of these is the case, we can skip this field.
|
|
304
|
+
# Note: all our fields are annotated so there's no need to check if they're not
|
|
305
|
+
if not (
|
|
306
|
+
(has_args and issubclass(args[0], Payload))
|
|
307
|
+
or (not has_args and issubclass(field.annotation, Payload)) # type: ignore[arg-type]
|
|
308
|
+
):
|
|
309
|
+
continue
|
|
310
|
+
|
|
311
|
+
# Finally, call this method recursively for the field and update the messages with the results
|
|
312
|
+
# Note: All our fields are annotated as XmlEntityInfo, so they have the "path" attribute
|
|
313
|
+
messages.update(
|
|
314
|
+
self._from_tree_messages_inner(
|
|
315
|
+
raw,
|
|
316
|
+
envelope_type,
|
|
317
|
+
args[0],
|
|
318
|
+
root,
|
|
319
|
+
field.path, # type: ignore[attr-defined]
|
|
320
|
+
origin is list,
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
# Finally, return our dictionary of messages
|
|
325
|
+
return messages
|
|
326
|
+
|
|
327
|
+
def _from_tree_messages_inner(
|
|
328
|
+
self, raw: Element, envelope_type: Type[E], current_type: Type[P], root: str, tag: str, multi: bool
|
|
329
|
+
) -> Dict[str, Messages]:
|
|
330
|
+
"""Attempt to extract the messages from within the payload at the current level.
|
|
331
|
+
|
|
332
|
+
Arguments:
|
|
333
|
+
raw (Element): The raw data to be converted.
|
|
334
|
+
envelope_type (Type[Envelope]): The type of envelope being constructed.
|
|
335
|
+
current_type (Type[Payload]): The type of data being constructed.
|
|
336
|
+
root (str): The root of the dictionary, used to create the key for the messages.
|
|
337
|
+
tag (str): The tag of the current node being processed.
|
|
338
|
+
multi (bool): If True, the payload will be a multi-response; otherwise, it will be a single
|
|
339
|
+
response.
|
|
340
|
+
|
|
341
|
+
Returns: A dictionary mapping messages to where they were found in the response.
|
|
342
|
+
"""
|
|
343
|
+
# Construct the new root from the existing root and the tag
|
|
344
|
+
path_base = f"{root}.{tag}"
|
|
345
|
+
|
|
346
|
+
# If we are processing a list, we need to iterate over each node and call this method recursively. Otherwise,
|
|
347
|
+
# we can call this method recursively for the node and update the messages with the results.
|
|
348
|
+
if multi:
|
|
349
|
+
# Attempt to find all the nodes with the given tag; if we don't find any then we'll return empty.
|
|
350
|
+
nodes = raw.findall(tag)
|
|
351
|
+
if not nodes:
|
|
352
|
+
return {}
|
|
353
|
+
|
|
354
|
+
# Otherwise, we'll call this method recursively for each node and update the messages with the results.
|
|
355
|
+
messages = {}
|
|
356
|
+
for i, node in enumerate(nodes):
|
|
357
|
+
messages.update(self._from_tree_messages(node, envelope_type, current_type, f"{path_base}[{i}]", True))
|
|
358
|
+
return messages
|
|
359
|
+
|
|
360
|
+
# If we reached this point then we are processing a single item so find the associated
|
|
361
|
+
child = raw.find(tag)
|
|
362
|
+
return {} if child is None else self._from_tree_messages(child, envelope_type, current_type, path_base, False)
|
|
363
|
+
|
|
364
|
+
def _from_xml(self, data: bytes) -> Element:
|
|
365
|
+
"""Parse the XML file, returning the resulting XML tree.
|
|
366
|
+
|
|
367
|
+
Arguments:
|
|
368
|
+
data: The raw XML data to be parsed.
|
|
369
|
+
|
|
370
|
+
Returns: A parsed and validated XML tree containing the data.
|
|
371
|
+
"""
|
|
372
|
+
# First, get a file-like reference to the data
|
|
373
|
+
dat_file = BytesIO(data)
|
|
374
|
+
|
|
375
|
+
# Next, parse the XML data into an XML element tree
|
|
376
|
+
doc = parse(dat_file)
|
|
377
|
+
|
|
378
|
+
# Now, verify that the XML data is valid according to the schema
|
|
379
|
+
self._schema.assertValid(doc)
|
|
380
|
+
|
|
381
|
+
# Finally, return the results
|
|
382
|
+
return doc.getroot()
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
# Create the response payload type
|
|
386
|
+
PayloadType = Type[Union[Response[E, P], MultiResponse[E, P]]]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@lru_cache(maxsize=None)
|
|
390
|
+
def _create_response_payload_type(key: str, envelope_type: Type[E], data_type: Type[P], multi: bool) -> PayloadType:
|
|
391
|
+
"""Create a new payload type for the given envelope and data types.
|
|
392
|
+
|
|
393
|
+
This method is intended to save us the overhead of writing a new class for each payload type. Instead, we can
|
|
394
|
+
create a new class at runtime that contains the envelope and data types, and use that for deserialization.
|
|
395
|
+
|
|
396
|
+
Note that this method has been LRU-cached because the operation of creating a new class from data types at
|
|
397
|
+
runtime involves lots of reflection and is quite slow. By caching the results, we can avoid the overhead of
|
|
398
|
+
creating new classes every time we need to deserialize data.
|
|
399
|
+
|
|
400
|
+
Arguments:
|
|
401
|
+
key (str): The tag to use for the parent element of the payload.
|
|
402
|
+
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
403
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
404
|
+
multi (bool): If True, the payload will be a multi-response; otherwise, it will be a single
|
|
405
|
+
response.
|
|
406
|
+
|
|
407
|
+
Returns: The base response type that will be used for deserialization.
|
|
408
|
+
"""
|
|
409
|
+
# First, create our base response type depending on whether we are creating a multi-response or not
|
|
410
|
+
base_type: PayloadType = Response[envelope_type, data_type] # type: ignore[valid-type]
|
|
411
|
+
if multi:
|
|
412
|
+
base_type = MultiResponse[envelope_type, data_type] # type: ignore[valid-type]
|
|
413
|
+
|
|
414
|
+
# Next, create a new payload type that contains the envelope and data types with the appropriate XML tag
|
|
415
|
+
class RSPayload(base_type, tag=key): # type: ignore[call-arg, valid-type, misc]
|
|
416
|
+
"""Wrapper for the response payload type that will be used for serialization."""
|
|
417
|
+
|
|
418
|
+
# Finally, return the payload type so we can instantiate it
|
|
419
|
+
return RSPayload
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
@lru_cache(maxsize=None)
|
|
423
|
+
def _create_response_common_type(tag_type: Type) -> Type[ResponseCommon]:
|
|
424
|
+
"""Create a new wrapper for the ResponseCommon type with the given tag.
|
|
425
|
+
|
|
426
|
+
This method is intended to save us the overhead of writing a new class for each tag type. Instead, we can
|
|
427
|
+
create a new class at runtime that contains the ResponseCommon type, and use that for deserialization.
|
|
428
|
+
|
|
429
|
+
Arguments:
|
|
430
|
+
tag_type (Type): The type of tag to use for the wrapper.
|
|
431
|
+
|
|
432
|
+
Returns: The wrapper type that will be used for deserialization.
|
|
433
|
+
""" # fmt: skip
|
|
434
|
+
# First, create a new wrapper type that contains the ResponseCommon type with the appropriate XML tag
|
|
435
|
+
class Wrapper(ResponseCommon, tag=tag_type.__name__): # type: ignore[call-arg]
|
|
436
|
+
"""Wrapper for the validation object with the proper XML tag."""
|
|
437
|
+
|
|
438
|
+
# Finally, return the wrapper type so we can instantiate it
|
|
439
|
+
return Wrapper
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@lru_cache(maxsize=None)
|
|
443
|
+
def _create_request_payload_type(key: str, envelope_type: Type[E], data_type: Type[P]) -> Type[PayloadBase]:
|
|
444
|
+
"""Create a new payload type for the given payload and data types.
|
|
445
|
+
|
|
446
|
+
This method is intended to save us the overhead of writing a new class for each payload type. Instead, we can
|
|
447
|
+
create a new class at runtime that contains the payload and data types, and use that for serialization.
|
|
448
|
+
|
|
449
|
+
Note that this method has been LRU-cached because the operation of creating a new class from data types at
|
|
450
|
+
runtime involves lots of reflection and is quite slow. By caching the results, we can avoid the overhead of
|
|
451
|
+
creating new classes every time we need to serialize data.
|
|
452
|
+
|
|
453
|
+
Arguments:
|
|
454
|
+
key: str The tag to use for the parent element of the payload.
|
|
455
|
+
envelope_type (Type[Envelope]): The type of payload to be constructed.
|
|
456
|
+
data_type (Type[Payload]): The type of data to be constructed.
|
|
457
|
+
|
|
458
|
+
Returns: A new payload type that can be used for serialization.
|
|
459
|
+
""" # fmt: skip
|
|
460
|
+
# First, create a wrapper for our data type that will be used to store the data in the payload
|
|
461
|
+
class Envelope(envelope_type): # type: ignore[valid-type, misc]
|
|
462
|
+
"""Wrapper for the data type that will be used to store the data in the payload."""
|
|
463
|
+
|
|
464
|
+
# The data to be stored in the payload
|
|
465
|
+
data: data_type = element(tag=data_type.__name__) # type: ignore[valid-type]
|
|
466
|
+
|
|
467
|
+
def __init__(self, envelope: envelope_type, data: data_type): # type: ignore[valid-type]
|
|
468
|
+
"""Create a new envelope to store payload data.
|
|
469
|
+
|
|
470
|
+
Arguments:
|
|
471
|
+
envelope (Envelope): The payload to be stored in the data.
|
|
472
|
+
data (Payload): The data to be stored in the payload.
|
|
473
|
+
"""
|
|
474
|
+
obj = dict(envelope)
|
|
475
|
+
obj["data"] = data
|
|
476
|
+
super().__init__(**obj)
|
|
477
|
+
|
|
478
|
+
# Next, create our payload type that actually contains all the XML data
|
|
479
|
+
class RQPayload(PayloadBase, tag=key): # type: ignore[call-arg]
|
|
480
|
+
"""The payload type that will be used for serialization."""
|
|
481
|
+
|
|
482
|
+
# The payload containing our request object and any data
|
|
483
|
+
envelope: Envelope = element(tag=envelope_type.__name__)
|
|
484
|
+
|
|
485
|
+
def __init__(self, envelope: envelope_type, data: data_type, schema: str): # type: ignore[valid-type]
|
|
486
|
+
"""Create a new payload containing the request object and any data.
|
|
487
|
+
|
|
488
|
+
Arguments:
|
|
489
|
+
envelope (Envelope): The payload to be stored in the data.
|
|
490
|
+
data (Payload): The data to be stored in the payload.
|
|
491
|
+
schema (str): The name of the schema file to use for validation.
|
|
492
|
+
"""
|
|
493
|
+
super().__init__(location=schema, envelope=Envelope(envelope, data))
|
|
494
|
+
|
|
495
|
+
# Finally, return the payload type so we can instantiate it
|
|
496
|
+
return RQPayload
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _find_or_fail(node: Element, tag: str) -> Element:
|
|
500
|
+
"""Find the node with the given tag, or raise an error if it isn't found.
|
|
501
|
+
|
|
502
|
+
Arguments:
|
|
503
|
+
node (Element): The node to search for the tag.
|
|
504
|
+
tag (str): The tag to search for in the node.
|
|
505
|
+
|
|
506
|
+
Returns: The node with the given tag.
|
|
507
|
+
|
|
508
|
+
Raises: ValueError if the tag isn't found in the node.
|
|
509
|
+
"""
|
|
510
|
+
found = node.find(tag)
|
|
511
|
+
if found is None:
|
|
512
|
+
raise ValueError(f"Expected tag '{tag}' not found in node") # pragma: no cover
|
|
513
|
+
return found
|