mms-client 1.6.0__py3-none-any.whl → 1.8.0__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/security/certs.py +34 -1
- mms_client/security/crypto.py +30 -22
- mms_client/services/base.py +133 -66
- mms_client/services/market.py +31 -12
- mms_client/services/registration.py +12 -9
- mms_client/services/report.py +144 -1
- mms_client/types/base.py +53 -13
- mms_client/types/enums.py +34 -0
- mms_client/types/report.py +474 -0
- mms_client/types/resource.py +4 -34
- mms_client/types/transport.py +2 -2
- mms_client/utils/multipart_transport.py +259 -0
- mms_client/utils/serialization.py +137 -44
- mms_client/utils/web.py +16 -2
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/METADATA +14 -5
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/RECORD +18 -16
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/LICENSE +0 -0
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Contains a transport override supporting MTOMS attachments."""
|
|
2
|
+
|
|
3
|
+
import string
|
|
4
|
+
from base64 import b64encode
|
|
5
|
+
from email.encoders import encode_7or8bit
|
|
6
|
+
from email.mime.application import MIMEApplication
|
|
7
|
+
from email.mime.base import MIMEBase
|
|
8
|
+
from email.mime.multipart import MIMEMultipart
|
|
9
|
+
from logging import getLogger
|
|
10
|
+
from random import SystemRandom
|
|
11
|
+
from typing import Dict
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from lxml import etree
|
|
15
|
+
from lxml.etree import _Element as Element
|
|
16
|
+
from pendulum import now
|
|
17
|
+
from requests import Session
|
|
18
|
+
from zeep.cache import VersionedCacheBase
|
|
19
|
+
from zeep.transports import Transport
|
|
20
|
+
from zeep.wsdl.utils import etree_to_string
|
|
21
|
+
|
|
22
|
+
# Set the default logger for the MMS client
|
|
23
|
+
logger = getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Define the namespaces used in the XML
|
|
27
|
+
XOP = "http://www.w3.org/2004/08/xop/include"
|
|
28
|
+
XMIME5 = "http://www.w3.org/2005/05/xmlmime"
|
|
29
|
+
FILETAG = "xop:Include:"
|
|
30
|
+
ID_LEN = 16
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Define a function to generate a randomized ID string
|
|
34
|
+
def get_id(length: int = ID_LEN) -> str:
|
|
35
|
+
"""Generate a randomized ID string.
|
|
36
|
+
|
|
37
|
+
Arguments:
|
|
38
|
+
length (int): The length of the ID string to generate.
|
|
39
|
+
|
|
40
|
+
Returns: The randomized ID string.
|
|
41
|
+
"""
|
|
42
|
+
return "".join(SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def now_b64():
|
|
46
|
+
"""Return a base64 encoded string of the current timestamp."""
|
|
47
|
+
return b64encode(f"{now().timestamp()}".replace(".", "").encode("UTF-8")).decode("UTF-8")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_boundary() -> str:
|
|
51
|
+
"""Return a randomized MIME boundary string."""
|
|
52
|
+
return f"MIMEBoundary_{now_b64()}".center(33, "=")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_content_id(domain: str) -> str:
|
|
56
|
+
"""Return a randomized MIME content ID string.
|
|
57
|
+
|
|
58
|
+
Arguments:
|
|
59
|
+
domain (str): The domain of the content ID.
|
|
60
|
+
|
|
61
|
+
Returns: The randomized MIME content ID string.
|
|
62
|
+
"""
|
|
63
|
+
return f"<{now_b64()}@{domain}>"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def overwrite_attachnode(node):
|
|
67
|
+
"""Overwrite the attachment node.
|
|
68
|
+
|
|
69
|
+
Arguments:
|
|
70
|
+
node (Element): The XML node to be overwritten.
|
|
71
|
+
|
|
72
|
+
Returns: The attachment node.
|
|
73
|
+
"""
|
|
74
|
+
cid = node.text[len(FILETAG) :]
|
|
75
|
+
node.text = None
|
|
76
|
+
etree.SubElement(node, f"{{{XOP}}}Include", nsmap={"xop": XOP}, href=f"cid:{cid}")
|
|
77
|
+
return cid
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def get_multipart(content_id: str) -> MIMEMultipart:
|
|
81
|
+
"""Create a MIME multipart object.
|
|
82
|
+
|
|
83
|
+
Arguments:
|
|
84
|
+
content_id (str): The content ID to be used for the attachment.
|
|
85
|
+
|
|
86
|
+
Returns: The MIME multipart object.
|
|
87
|
+
"""
|
|
88
|
+
part = MIMEMultipart(
|
|
89
|
+
"related", charset="UTF-8", type="application/xop+xml", boundary=get_boundary(), start=content_id
|
|
90
|
+
)
|
|
91
|
+
part.set_param("start-info", "application/soap+xml")
|
|
92
|
+
return part
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_envelope_part(envelope: Element, content_id: str) -> MIMEApplication:
|
|
96
|
+
"""Create the MIME envelope part.
|
|
97
|
+
|
|
98
|
+
Arguments:
|
|
99
|
+
envelope (Element): The XML envelope to be attached.
|
|
100
|
+
content_id (str): The content ID to be used for the attachment.
|
|
101
|
+
|
|
102
|
+
Returns: The MIME envelope part.
|
|
103
|
+
"""
|
|
104
|
+
part = MIMEApplication(etree_to_string(envelope), "xop+xml", encode_7or8bit)
|
|
105
|
+
part.set_param("charset", "utf-8")
|
|
106
|
+
part.set_param("type", "text/xml")
|
|
107
|
+
part.add_header("Content-ID", content_id)
|
|
108
|
+
part.add_header("Content-Transfer-Encoding", "binary")
|
|
109
|
+
return part
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Attachment:
|
|
113
|
+
"""Represents an attachment to be sent with a multipart request."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, name: str, data: bytes, domain: str):
|
|
116
|
+
"""Create a new attachment.
|
|
117
|
+
|
|
118
|
+
Arguments:
|
|
119
|
+
name (str): The name of the attachment.
|
|
120
|
+
data (bytes): The data to be attached.
|
|
121
|
+
domain (str): The domain of the content ID.
|
|
122
|
+
"""
|
|
123
|
+
self.name = name
|
|
124
|
+
self.data = data
|
|
125
|
+
self.cid = f"{get_id()}-{name}@{domain}"
|
|
126
|
+
self.tag = FILETAG + self.cid
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class MultipartTransport(Transport):
|
|
130
|
+
"""A transport that supports MTOMS attachments."""
|
|
131
|
+
|
|
132
|
+
def __init__(
|
|
133
|
+
self,
|
|
134
|
+
domain: str,
|
|
135
|
+
cache: Optional[VersionedCacheBase] = None,
|
|
136
|
+
timeout: int = 300,
|
|
137
|
+
operation_timeout: Optional[int] = None,
|
|
138
|
+
session: Optional[Session] = None,
|
|
139
|
+
):
|
|
140
|
+
"""Create a new MTOMS transport.
|
|
141
|
+
|
|
142
|
+
Arguments:
|
|
143
|
+
domain (str): The domain of the content ID to use.
|
|
144
|
+
cache (VersionedCacheBase, optional): The cache to be used for the transport.
|
|
145
|
+
timeout (int): The timeout for the transport.
|
|
146
|
+
operation_timeout (int, optional): The operation timeout for the transport.
|
|
147
|
+
session (Session, optional): The session to be used for the transport.
|
|
148
|
+
"""
|
|
149
|
+
# Save the domain for later use
|
|
150
|
+
self._domain = domain
|
|
151
|
+
|
|
152
|
+
# Setup a dictionary to store the attachments after they're registered
|
|
153
|
+
self._attachments: Dict[str, Attachment] = {}
|
|
154
|
+
|
|
155
|
+
# Call the parent constructor
|
|
156
|
+
super().__init__(
|
|
157
|
+
cache=cache,
|
|
158
|
+
timeout=timeout,
|
|
159
|
+
operation_timeout=operation_timeout,
|
|
160
|
+
session=session,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def register_attachment(self, name: str, data: bytes) -> str:
|
|
164
|
+
"""Register an attachment.
|
|
165
|
+
|
|
166
|
+
Registered attachments will be sent with the request as MTOMS attachments. The content ID of the attachment
|
|
167
|
+
will be returned so that it can be used in the request.
|
|
168
|
+
|
|
169
|
+
Arguments:
|
|
170
|
+
name (str): The name of the attachment.
|
|
171
|
+
data (bytes): The data to be attached.
|
|
172
|
+
|
|
173
|
+
Returns: The content ID of the attachment, which should be used in place of the attachment data.
|
|
174
|
+
"""
|
|
175
|
+
attachment = Attachment(name, data, self._domain)
|
|
176
|
+
self._attachments[attachment.cid] = attachment
|
|
177
|
+
return attachment.tag
|
|
178
|
+
|
|
179
|
+
def post_xml(self, address: str, envelope: Element, headers: dict):
|
|
180
|
+
"""Post the XML envelope and attachments.
|
|
181
|
+
|
|
182
|
+
Arguments:
|
|
183
|
+
address (str): The address to post the data to.
|
|
184
|
+
envelope (Element): The XML envelope to be attached.
|
|
185
|
+
headers (dict): The headers to be used for the request.
|
|
186
|
+
|
|
187
|
+
Returns: The response from the server.
|
|
188
|
+
"""
|
|
189
|
+
# Search for values that start with our FILETAG
|
|
190
|
+
filetags = envelope.xpath(f"//*[starts-with(text(), '{FILETAG}')]")
|
|
191
|
+
|
|
192
|
+
# if there are any attached files, we will set the attachments. Otherwise, just the envelope
|
|
193
|
+
if filetags:
|
|
194
|
+
message = self.create_mtom_request(filetags, envelope, headers).encode("UTF-8")
|
|
195
|
+
else:
|
|
196
|
+
message = etree_to_string(envelope)
|
|
197
|
+
|
|
198
|
+
# Post the request and return the response
|
|
199
|
+
return self.post(address, message, headers)
|
|
200
|
+
|
|
201
|
+
def create_mtom_request(self, filetags, envelope: Element, headers: dict) -> str:
|
|
202
|
+
"""Set MTOM attachments and return the right envelope.
|
|
203
|
+
|
|
204
|
+
Arguments:
|
|
205
|
+
filetags (list): The list of XML paths to the attachments.
|
|
206
|
+
envelope (Element): The XML envelope to be attached.
|
|
207
|
+
headers (dict): The headers to be used for the request.
|
|
208
|
+
|
|
209
|
+
Returns: The XML envelope with the attachments.
|
|
210
|
+
"""
|
|
211
|
+
# First, get an identifier for the request and then use it to create a new multipart request
|
|
212
|
+
content_id = get_content_id(self._domain)
|
|
213
|
+
mtom_part = get_multipart(content_id)
|
|
214
|
+
|
|
215
|
+
# Next, let's set the XOP:Include nodes for each attachment
|
|
216
|
+
files = [overwrite_attachnode(f) for f in filetags]
|
|
217
|
+
|
|
218
|
+
# Now, create the request envelope and attach it to the multipart request
|
|
219
|
+
env_part = get_envelope_part(envelope, content_id)
|
|
220
|
+
mtom_part.attach(env_part)
|
|
221
|
+
|
|
222
|
+
# Attach each file to the multipart request
|
|
223
|
+
for cid in files:
|
|
224
|
+
mtom_part.attach(self.create_attachment(cid))
|
|
225
|
+
|
|
226
|
+
# Finally, create the final multipart request string
|
|
227
|
+
bound = f"--{mtom_part.get_boundary()}"
|
|
228
|
+
marray = mtom_part.as_string().split(bound)
|
|
229
|
+
mtombody = bound + bound.join(marray[1:])
|
|
230
|
+
|
|
231
|
+
# Set the content length and add the MTOM headers to the request
|
|
232
|
+
mtom_part.add_header("Content-Length", str(len(mtombody)))
|
|
233
|
+
headers.update(dict(mtom_part.items()))
|
|
234
|
+
|
|
235
|
+
# Decode the XML and return the request
|
|
236
|
+
message = mtom_part.as_string().split("\n\n", 1)[1]
|
|
237
|
+
message = message.replace("\n", "\r\n", 5)
|
|
238
|
+
return message
|
|
239
|
+
|
|
240
|
+
def create_attachment(self, cid):
|
|
241
|
+
"""Create an attachment for the multipart request.
|
|
242
|
+
|
|
243
|
+
Arguments:
|
|
244
|
+
cid (str): The content ID of the attachment.
|
|
245
|
+
|
|
246
|
+
Returns: The attachment.
|
|
247
|
+
"""
|
|
248
|
+
# First, get the attachment from the cache
|
|
249
|
+
attach = self._attachments[cid]
|
|
250
|
+
|
|
251
|
+
# Next, create the attachment
|
|
252
|
+
part = MIMEBase("application", "octet-stream")
|
|
253
|
+
part["Content-Transfer-Encoding"] = "binary"
|
|
254
|
+
part["Content-ID"] = f"<{attach.cid}>"
|
|
255
|
+
part.set_payload(attach.data, charset="utf-8")
|
|
256
|
+
del part["mime-version"]
|
|
257
|
+
|
|
258
|
+
# Finally, return the attachment
|
|
259
|
+
return part
|
|
@@ -56,89 +56,128 @@ class Serializer:
|
|
|
56
56
|
with open(XSD_DIR / self._xsd.value, "rb") as f:
|
|
57
57
|
self._schema = XMLSchema(parse(f))
|
|
58
58
|
|
|
59
|
-
def serialize(self, request_envelope: E, request_data: P) -> bytes:
|
|
59
|
+
def serialize(self, request_envelope: E, request_data: P, for_report: bool = False) -> bytes:
|
|
60
60
|
"""Serialize the envelope and data to a byte string for sending to the MMS server.
|
|
61
61
|
|
|
62
62
|
Arguments:
|
|
63
63
|
request_envelope (Envelope): The envelope to be serialized.
|
|
64
64
|
request_data (Payload): The data to be serialized.
|
|
65
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
65
66
|
|
|
66
67
|
Returns: A byte string containing the XML-formatted data to be sent to the MMS server.
|
|
67
68
|
"""
|
|
68
|
-
# First,
|
|
69
|
-
|
|
69
|
+
# First, choose the correct payload factory based on the request type
|
|
70
|
+
factory = _create_report_payload_type if for_report else _create_request_payload_type
|
|
71
|
+
|
|
72
|
+
# Next, create our payload class from the payload and data types
|
|
73
|
+
payload_cls = factory(
|
|
70
74
|
self._payload_key,
|
|
71
75
|
type(request_envelope),
|
|
72
76
|
type(request_data),
|
|
73
77
|
False, # type: ignore[arg-type]
|
|
74
78
|
)
|
|
75
79
|
|
|
76
|
-
#
|
|
80
|
+
# Now, inject the payload and data into the payload class
|
|
77
81
|
# NOTE: this returns a type that inherits from PayloadBase and the arguments provided to the initializer
|
|
78
82
|
# here are correct, but mypy thinks they are incorrect because it doesn't understand the the inherited type
|
|
79
83
|
payload = payload_cls(request_envelope, request_data, self._xsd.value) # type: ignore[call-arg, misc]
|
|
80
84
|
|
|
81
85
|
# Finally, convert the payload to XML and return it
|
|
82
86
|
# NOTE: we provided the encoding here so this will return bytes, not a string
|
|
83
|
-
return
|
|
87
|
+
return self._to_canoncialized_xml(payload)
|
|
84
88
|
|
|
85
|
-
def serialize_multi(
|
|
89
|
+
def serialize_multi(
|
|
90
|
+
self, request_envelope: E, request_data: List[P], request_type: Type[P], for_report: bool = False
|
|
91
|
+
) -> bytes:
|
|
86
92
|
"""Serialize the envelope and data to a byte string for sending to the MMS server.
|
|
87
93
|
|
|
88
94
|
Arguments:
|
|
89
95
|
request_envelope (Envelope): The envelope to be serialized.
|
|
90
|
-
request_data (List[Payload]):
|
|
91
|
-
request_type (Type[Payload]):
|
|
96
|
+
request_data (List[Payload]): The data to be serialized.
|
|
97
|
+
request_type (Type[Payload]): The type of data to be serialized.
|
|
98
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
92
99
|
|
|
93
100
|
Returns: A byte string containing the XML-formatted data to be sent to the MMS server.
|
|
94
101
|
"""
|
|
95
|
-
# First,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
# First, choose the correct payload factory based on the request type
|
|
103
|
+
factory = _create_report_payload_type if for_report else _create_request_payload_type
|
|
104
|
+
|
|
105
|
+
# Next, create our payload class from the payload and data types
|
|
106
|
+
payload_cls = factory(self._payload_key, type(request_envelope), request_type, True) # type: ignore[arg-type]
|
|
99
107
|
|
|
100
|
-
#
|
|
108
|
+
# Now, inject the payload and data into the payload class
|
|
101
109
|
# NOTE: this returns a type that inherits from PayloadBase and the arguments provided to the initializer
|
|
102
110
|
# here are correct, but mypy thinks they are incorrect because it doesn't understand the the inherited type
|
|
103
111
|
payload = payload_cls(request_envelope, request_data, self._xsd.value) # type: ignore[call-arg, misc]
|
|
104
112
|
|
|
105
113
|
# Finally, convert the payload to XML and return it
|
|
106
114
|
# NOTE: we provided the encoding here so this will return bytes, not a string
|
|
107
|
-
return
|
|
115
|
+
return self._to_canoncialized_xml(payload)
|
|
108
116
|
|
|
109
|
-
def deserialize(
|
|
117
|
+
def deserialize(
|
|
118
|
+
self, data: bytes, envelope_type: Type[E], data_type: Type[P], for_report: bool = False
|
|
119
|
+
) -> Response[E, P]:
|
|
110
120
|
"""Deserialize the data to a response object.
|
|
111
121
|
|
|
112
122
|
Arguments:
|
|
113
123
|
data (bytes): The raw data to be deserialized.
|
|
114
124
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
115
125
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
126
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
116
127
|
|
|
117
128
|
Returns: A response object containing the envelope and data extracted from the raw data.
|
|
118
129
|
"""
|
|
119
130
|
tree = self._from_xml(data)
|
|
120
|
-
return self._from_tree(tree, envelope_type, data_type)
|
|
131
|
+
return self._from_tree(tree, envelope_type, data_type, for_report)
|
|
121
132
|
|
|
122
|
-
def deserialize_multi(
|
|
133
|
+
def deserialize_multi(
|
|
134
|
+
self, data: bytes, envelope_type: Type[E], data_type: Type[P], for_report: bool = False
|
|
135
|
+
) -> MultiResponse[E, P]:
|
|
123
136
|
"""Deserialize the data to a multi-response object.
|
|
124
137
|
|
|
125
138
|
Arguments:
|
|
126
139
|
data (bytes): The raw data to be deserialized.
|
|
127
140
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
128
141
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
142
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
129
143
|
|
|
130
144
|
Returns: A multi-response object containing the envelope and data extracted from the raw data.
|
|
131
145
|
"""
|
|
132
146
|
tree = self._from_xml(data)
|
|
133
|
-
return self._from_tree_multi(tree, envelope_type, data_type)
|
|
147
|
+
return self._from_tree_multi(tree, envelope_type, data_type, for_report)
|
|
148
|
+
|
|
149
|
+
def _to_canoncialized_xml(self, payload: PayloadBase) -> bytes:
|
|
150
|
+
"""Convert the payload to a canonicalized XML string.
|
|
151
|
+
|
|
152
|
+
Arguments:
|
|
153
|
+
payload (PayloadBase): The payload to be converted.
|
|
134
154
|
|
|
135
|
-
|
|
155
|
+
Returns: The canonicalized XML string.
|
|
156
|
+
"""
|
|
157
|
+
# First, convert the payload to a raw XML string
|
|
158
|
+
raw: bytes = payload.to_xml(
|
|
159
|
+
skip_empty=True,
|
|
160
|
+
encoding="utf-8",
|
|
161
|
+
xml_declaration=False,
|
|
162
|
+
) # type: ignore[assignment]
|
|
163
|
+
|
|
164
|
+
# Next, parse it back into an XML tree
|
|
165
|
+
unparsed = parse(BytesIO(raw))
|
|
166
|
+
|
|
167
|
+
# Finally, convert the XML tree to a canonicalized XML string and return it
|
|
168
|
+
buffer = BytesIO()
|
|
169
|
+
unparsed.write_c14n(buffer)
|
|
170
|
+
buffer.seek(0)
|
|
171
|
+
return buffer.read()
|
|
172
|
+
|
|
173
|
+
def _from_tree(self, raw: Element, envelope_type: Type[E], data_type: Type[P], for_report: bool) -> Response[E, P]:
|
|
136
174
|
"""Convert the raw data to a response object.
|
|
137
175
|
|
|
138
176
|
Arguments:
|
|
139
177
|
raw (Element): The raw data to be converted.
|
|
140
178
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
141
179
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
180
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
142
181
|
|
|
143
182
|
Returns: A response object containing the envelope and data extracted from the raw data.
|
|
144
183
|
"""
|
|
@@ -155,7 +194,9 @@ class Serializer:
|
|
|
155
194
|
resp = cls.from_xml_tree(raw) # type: ignore[arg-type]
|
|
156
195
|
|
|
157
196
|
# Next, attempt to extract the envelope and data from within the response
|
|
158
|
-
resp.envelope, resp.envelope_validation, envelope_node = self._from_tree_envelope(
|
|
197
|
+
resp.envelope, resp.envelope_validation, envelope_node = self._from_tree_envelope(
|
|
198
|
+
raw, envelope_type, for_report
|
|
199
|
+
)
|
|
159
200
|
|
|
160
201
|
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
161
202
|
# from within the envelope
|
|
@@ -163,18 +204,23 @@ class Serializer:
|
|
|
163
204
|
resp.payload = self._from_tree_data(envelope_node.find(get_tag(data_type)), data_type)
|
|
164
205
|
|
|
165
206
|
# Finally, attempt to extract the messages from within the payload
|
|
166
|
-
resp.messages = self._from_tree_messages(
|
|
207
|
+
resp.messages = self._from_tree_messages(
|
|
208
|
+
raw, get_tag(envelope_type), data_type, self._payload_key, False, for_report=for_report
|
|
209
|
+
)
|
|
167
210
|
|
|
168
211
|
# Return the response
|
|
169
212
|
return resp
|
|
170
213
|
|
|
171
|
-
def _from_tree_multi(
|
|
214
|
+
def _from_tree_multi(
|
|
215
|
+
self, raw: Element, envelope_type: Type[E], data_type: Type[P], for_report: bool
|
|
216
|
+
) -> MultiResponse[E, P]:
|
|
172
217
|
"""Convert the raw data to a multi-response object.
|
|
173
218
|
|
|
174
219
|
Arguments:
|
|
175
220
|
raw (Element): The raw data to be converted.
|
|
176
221
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
177
222
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
223
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
178
224
|
|
|
179
225
|
Returns: A multi-response object containing the envelope and data extracted from the raw data.
|
|
180
226
|
"""
|
|
@@ -191,7 +237,7 @@ class Serializer:
|
|
|
191
237
|
resp = cls.from_xml_tree(raw) # type: ignore[arg-type]
|
|
192
238
|
|
|
193
239
|
# Next, attempt to extract the envelope from the response
|
|
194
|
-
resp.envelope, resp.envelope_validation, env_node = self._from_tree_envelope(raw, envelope_type)
|
|
240
|
+
resp.envelope, resp.envelope_validation, env_node = self._from_tree_envelope(raw, envelope_type, for_report)
|
|
195
241
|
|
|
196
242
|
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
197
243
|
# from within the envelope
|
|
@@ -202,17 +248,22 @@ class Serializer:
|
|
|
202
248
|
]
|
|
203
249
|
|
|
204
250
|
# Finally, attempt to extract the messages from within the payload
|
|
205
|
-
resp.messages = self._from_tree_messages(
|
|
251
|
+
resp.messages = self._from_tree_messages(
|
|
252
|
+
raw, get_tag(envelope_type), data_type, self._payload_key, True, for_report=for_report
|
|
253
|
+
)
|
|
206
254
|
|
|
207
255
|
# Return the response
|
|
208
256
|
return resp
|
|
209
257
|
|
|
210
|
-
def _from_tree_envelope(
|
|
258
|
+
def _from_tree_envelope(
|
|
259
|
+
self, raw: Element, envelope_type: Type[E], for_report: bool
|
|
260
|
+
) -> Tuple[E, ResponseCommon, Element]:
|
|
211
261
|
"""Attempt to extract the envelope from within the response.
|
|
212
262
|
|
|
213
263
|
Arguments:
|
|
214
264
|
raw (Element): The raw data to be converted.
|
|
215
265
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
266
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
216
267
|
|
|
217
268
|
Returns:
|
|
218
269
|
Envelope: The request payload constructed from the raw data.
|
|
@@ -220,9 +271,10 @@ class Serializer:
|
|
|
220
271
|
"""
|
|
221
272
|
# First, attempt to extract the envelope from within the response; if the key isn't found then we'll raise an
|
|
222
273
|
# exception to indicate that the envelope wasn't found.
|
|
223
|
-
|
|
224
|
-
if
|
|
225
|
-
|
|
274
|
+
envelope_tag = get_tag(envelope_type)
|
|
275
|
+
envelope_node = raw if for_report else raw.find(envelope_tag)
|
|
276
|
+
if envelope_node is None or envelope_node.tag != envelope_tag:
|
|
277
|
+
raise ValueError(f"Expected envelope type '{envelope_tag}' not found in response")
|
|
226
278
|
|
|
227
279
|
# Next, create a new envelope type that contains the envelope type with the appropriate XML tag. We have to do
|
|
228
280
|
# this because the envelope type doesn't include the ResponseCommon fields, and the tag doesn't match
|
|
@@ -247,7 +299,7 @@ class Serializer:
|
|
|
247
299
|
ValueError: If the expected data type is not found in the response.
|
|
248
300
|
"""
|
|
249
301
|
data_tags = set(node.tag for node in raw)
|
|
250
|
-
if not data_tags.issubset([data_type.__name__, data_type.__xml_tag__, "Messages"]):
|
|
302
|
+
if not data_tags.issubset([data_type.__name__, data_type.__xml_tag__, "ProcessingStatistics", "Messages"]):
|
|
251
303
|
raise ValueError(f"Expected data type '{data_type.__name__}' not found in response")
|
|
252
304
|
|
|
253
305
|
def _from_tree_data(self, raw: Optional[Element], data_type: Type[P]) -> Optional[ResponseData[P]]:
|
|
@@ -278,11 +330,12 @@ class Serializer:
|
|
|
278
330
|
def _from_tree_messages(
|
|
279
331
|
self,
|
|
280
332
|
raw: Element,
|
|
281
|
-
|
|
333
|
+
envelope_tag: str,
|
|
282
334
|
current_type: Type[P],
|
|
283
335
|
root: str,
|
|
284
336
|
multi: bool,
|
|
285
337
|
wrapped: bool = False,
|
|
338
|
+
for_report: bool = False,
|
|
286
339
|
) -> Dict[str, Messages]:
|
|
287
340
|
"""Attempt to extract the messages from within the payload.
|
|
288
341
|
|
|
@@ -292,13 +345,14 @@ class Serializer:
|
|
|
292
345
|
|
|
293
346
|
Arguments:
|
|
294
347
|
raw (Element): The raw data to be converted.
|
|
295
|
-
|
|
348
|
+
envelope_tag (str): The tag of the envelope being constructed.
|
|
296
349
|
current_type (Type[Payload]): The type of data being constructed.
|
|
297
350
|
root (str): The root of the dictionary, used to create the key for the messages.
|
|
298
351
|
multi (bool): Whether we're processing a list of nodes or a single node. If called with the
|
|
299
352
|
payload root, this value will determine whether we're processing a multi-
|
|
300
353
|
response or a single response.
|
|
301
354
|
wrapped (bool): Whether or not this type is referenced from a wrapped field.
|
|
355
|
+
for_report (bool): If True, the data will be serialized for a report request.
|
|
302
356
|
"""
|
|
303
357
|
# First, find the Messages node in the raw data
|
|
304
358
|
message_node = raw.find("Messages")
|
|
@@ -313,21 +367,21 @@ class Serializer:
|
|
|
313
367
|
# at the root of the response, we need to call this method for the envelope type. If we are at the envelope
|
|
314
368
|
# type, we need to call this method for the data type. If we are at the data type, we need to call this method
|
|
315
369
|
# for each field in the data type that is a Payload type.
|
|
316
|
-
if root == self._payload_key:
|
|
370
|
+
if root == self._payload_key and not for_report:
|
|
317
371
|
messages.update(
|
|
318
372
|
self._from_tree_messages(
|
|
319
|
-
_find_or_fail(raw,
|
|
320
|
-
|
|
373
|
+
_find_or_fail(raw, envelope_tag),
|
|
374
|
+
envelope_tag,
|
|
321
375
|
current_type,
|
|
322
|
-
f"{root}.{
|
|
376
|
+
f"{root}.{envelope_tag}",
|
|
323
377
|
multi,
|
|
324
378
|
False,
|
|
325
379
|
)
|
|
326
380
|
)
|
|
327
|
-
elif root.endswith(
|
|
381
|
+
elif root.endswith(envelope_tag) or wrapped:
|
|
328
382
|
messages.update(
|
|
329
383
|
self._from_tree_messages_inner(
|
|
330
|
-
raw,
|
|
384
|
+
raw, envelope_tag, current_type, root, get_tag(current_type), multi, False
|
|
331
385
|
)
|
|
332
386
|
)
|
|
333
387
|
else:
|
|
@@ -350,7 +404,7 @@ class Serializer:
|
|
|
350
404
|
messages.update(
|
|
351
405
|
self._from_tree_messages_inner(
|
|
352
406
|
raw,
|
|
353
|
-
|
|
407
|
+
envelope_tag,
|
|
354
408
|
arg,
|
|
355
409
|
root,
|
|
356
410
|
field.path, # type: ignore[attr-defined]
|
|
@@ -365,7 +419,7 @@ class Serializer:
|
|
|
365
419
|
def _from_tree_messages_inner(
|
|
366
420
|
self,
|
|
367
421
|
raw: Element,
|
|
368
|
-
|
|
422
|
+
envelope_tag: str,
|
|
369
423
|
current_type: Type[P],
|
|
370
424
|
root: str,
|
|
371
425
|
tag: str,
|
|
@@ -376,7 +430,7 @@ class Serializer:
|
|
|
376
430
|
|
|
377
431
|
Arguments:
|
|
378
432
|
raw (Element): The raw data to be converted.
|
|
379
|
-
|
|
433
|
+
envelope_tag (str): The tag of the envelope being constructed.
|
|
380
434
|
current_type (Type[Payload]): The type of data being constructed.
|
|
381
435
|
root (str): The root of the dictionary, used to create the key for the messages.
|
|
382
436
|
tag (str): The tag of the current node being processed.
|
|
@@ -402,7 +456,7 @@ class Serializer:
|
|
|
402
456
|
for i, node in enumerate(nodes):
|
|
403
457
|
messages.update(
|
|
404
458
|
self._from_tree_messages(
|
|
405
|
-
node,
|
|
459
|
+
node, envelope_tag, current_type, path_base if wrapped else f"{path_base}[{i}]", True, wrapped
|
|
406
460
|
)
|
|
407
461
|
)
|
|
408
462
|
return messages
|
|
@@ -412,7 +466,7 @@ class Serializer:
|
|
|
412
466
|
return (
|
|
413
467
|
{}
|
|
414
468
|
if child is None
|
|
415
|
-
else self._from_tree_messages(child,
|
|
469
|
+
else self._from_tree_messages(child, envelope_tag, current_type, path_base, False, wrapped)
|
|
416
470
|
)
|
|
417
471
|
|
|
418
472
|
def _from_xml(self, data: bytes) -> Element:
|
|
@@ -497,7 +551,7 @@ def _create_response_common_type(tag_type: Type[Union[E, P]]) -> Type[ResponseCo
|
|
|
497
551
|
def _create_request_payload_type(
|
|
498
552
|
key: str,
|
|
499
553
|
envelope_type: Type[E],
|
|
500
|
-
data_type: Type[
|
|
554
|
+
data_type: Type[P],
|
|
501
555
|
multi: bool,
|
|
502
556
|
) -> Type[PayloadBase]:
|
|
503
557
|
"""Create a new payload type for the given payload and data types.
|
|
@@ -559,6 +613,45 @@ def _create_request_payload_type(
|
|
|
559
613
|
return RQPayload
|
|
560
614
|
|
|
561
615
|
|
|
616
|
+
@lru_cache(maxsize=None)
|
|
617
|
+
def _create_report_payload_type(key: str, envelope_type: Type[E], data_type: Type[P], multi: bool) -> Type[PayloadBase]:
|
|
618
|
+
"""Create a new payload type for the given report data request.
|
|
619
|
+
|
|
620
|
+
Arguments:
|
|
621
|
+
key: str The tag to use for the parent element of the payload.
|
|
622
|
+
envelope_type (Type[Envelope]): The type of payload to be used for the envelope.
|
|
623
|
+
data_type (Type[Payload]): The type of payload to be used for the data.
|
|
624
|
+
multi (bool): If True, the payload will be a list; otherwise, it will be a singleton.
|
|
625
|
+
|
|
626
|
+
Returns: The base payload type that will be used for serialization.
|
|
627
|
+
"""
|
|
628
|
+
# First, create our data type
|
|
629
|
+
payload_type = List[data_type] if multi else data_type # type: ignore[valid-type]
|
|
630
|
+
|
|
631
|
+
# Next, create a wrapper for our data type that will be used to store the data in the payload
|
|
632
|
+
class Envelope(PayloadBase, envelope_type, tag=key): # type: ignore[valid-type, misc]
|
|
633
|
+
"""Wrapper for the data type that will be used to store the data in the payload."""
|
|
634
|
+
|
|
635
|
+
# The data to be stored in the payload
|
|
636
|
+
data: payload_type = element(tag=get_tag(data_type)) # type: ignore[valid-type]
|
|
637
|
+
|
|
638
|
+
def __init__(self, envelope: envelope_type, data: payload_type, schema: str): # type: ignore[valid-type]
|
|
639
|
+
"""Create a new envelope to store payload data.
|
|
640
|
+
|
|
641
|
+
Arguments:
|
|
642
|
+
envelope (Envelope): The payload to be stored in the data.
|
|
643
|
+
data (Payload): The data to be stored in the payload.
|
|
644
|
+
schema (str): The name of the schema file to use for validation.
|
|
645
|
+
"""
|
|
646
|
+
obj = dict(envelope)
|
|
647
|
+
obj["data"] = data
|
|
648
|
+
obj["location"] = schema
|
|
649
|
+
super().__init__(**obj)
|
|
650
|
+
|
|
651
|
+
# Finally, return the payload type so we can instantiate it
|
|
652
|
+
return Envelope
|
|
653
|
+
|
|
654
|
+
|
|
562
655
|
def _find_or_fail(node: Element, tag: str) -> Element:
|
|
563
656
|
"""Find the node with the given tag, or raise an error if it isn't found.
|
|
564
657
|
|
|
@@ -615,7 +708,7 @@ def _get_field_typing(typ: Type) -> Tuple[Type, bool]:
|
|
|
615
708
|
return args[-1][0] if len(args) > 0 else typ, get_origin(origin_type) is list # typing: ignore[return-value]
|
|
616
709
|
|
|
617
710
|
|
|
618
|
-
def get_tag(data_type: Type[P]) -> str:
|
|
711
|
+
def get_tag(data_type: Union[Type[P], Type[E]]) -> str:
|
|
619
712
|
"""Get the tag for the given data type.
|
|
620
713
|
|
|
621
714
|
Arguments:
|