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
mms_client/security/certs.py
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
"""Contains functionality associated with certificates."""
|
|
2
2
|
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from ssl import PROTOCOL_TLSv1_2
|
|
4
5
|
from typing import Union
|
|
5
6
|
|
|
7
|
+
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
8
|
+
from cryptography.hazmat.primitives.serialization import Encoding
|
|
9
|
+
from cryptography.hazmat.primitives.serialization import NoEncryption
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import PrivateFormat
|
|
11
|
+
from cryptography.hazmat.primitives.serialization import PublicFormat
|
|
12
|
+
from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates
|
|
6
13
|
from requests_pkcs12 import Pkcs12Adapter
|
|
7
14
|
|
|
8
15
|
|
|
9
16
|
class Certificate:
|
|
10
17
|
"""Describes a certificate composed of a cert file and a key file."""
|
|
11
18
|
|
|
19
|
+
# The encoding to use for MMS certificates
|
|
20
|
+
encoding = Encoding.PEM
|
|
21
|
+
|
|
12
22
|
def __init__(self, cert: Union[str, Path, bytes], passphrase: str):
|
|
13
23
|
"""Create a new Certificate.
|
|
14
24
|
|
|
@@ -29,6 +39,13 @@ class Certificate:
|
|
|
29
39
|
# Save the passphrase
|
|
30
40
|
self._passphrase = passphrase
|
|
31
41
|
|
|
42
|
+
# Load the private key using the cryptography library
|
|
43
|
+
private_key, _, _ = load_key_and_certificates(self._cert, self._passphrase.encode("UTF-8"))
|
|
44
|
+
if isinstance(private_key, RSAPrivateKey):
|
|
45
|
+
self._private = private_key
|
|
46
|
+
else:
|
|
47
|
+
raise TypeError(f"Private key of type ({type(private_key).__name__}) was not expected.")
|
|
48
|
+
|
|
32
49
|
@property
|
|
33
50
|
def certificate(self) -> bytes:
|
|
34
51
|
"""Return the certificate data."""
|
|
@@ -39,6 +56,22 @@ class Certificate:
|
|
|
39
56
|
"""Return the full path to the passphrase."""
|
|
40
57
|
return self._passphrase
|
|
41
58
|
|
|
59
|
+
def public_key(self) -> bytes:
|
|
60
|
+
"""Extract the public key from the certificate.
|
|
61
|
+
|
|
62
|
+
Returns: The public key in PEM format.
|
|
63
|
+
"""
|
|
64
|
+
return self._private.public_key().public_bytes(Certificate.encoding, PublicFormat.PKCS1)
|
|
65
|
+
|
|
66
|
+
def private_key(self) -> bytes:
|
|
67
|
+
"""Extract the private key from the certificate.
|
|
68
|
+
|
|
69
|
+
THIS SHOULD NOT, UNDER ANY CIRCUMSTANCES, BE PRINTED OR LOGGED!!!
|
|
70
|
+
|
|
71
|
+
Returns: The private key in PEM format.
|
|
72
|
+
"""
|
|
73
|
+
return self._private.private_bytes(Certificate.encoding, PrivateFormat.PKCS8, NoEncryption())
|
|
74
|
+
|
|
42
75
|
def to_adapter(self) -> Pkcs12Adapter:
|
|
43
76
|
"""Convert the certificate to a Pkcs12Adapter."""
|
|
44
|
-
return Pkcs12Adapter(pkcs12_data=self._cert, pkcs12_password=self._passphrase)
|
|
77
|
+
return Pkcs12Adapter(pkcs12_data=self._cert, pkcs12_password=self._passphrase, ssl_protocol=PROTOCOL_TLSv1_2)
|
mms_client/security/crypto.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
"""Contains objects for cryptographic operations."""
|
|
2
2
|
|
|
3
|
+
from base64 import b64decode
|
|
3
4
|
from base64 import b64encode
|
|
4
|
-
from hashlib import sha256
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey
|
|
10
|
-
from cryptography.hazmat.primitives.serialization.pkcs12 import load_key_and_certificates
|
|
6
|
+
from Cryptodome.Hash import SHA256
|
|
7
|
+
from Cryptodome.PublicKey import RSA
|
|
8
|
+
from Cryptodome.Signature import pkcs1_15
|
|
11
9
|
|
|
12
10
|
from mms_client.security.certs import Certificate
|
|
13
11
|
|
|
@@ -21,23 +19,33 @@ class CryptoWrapper:
|
|
|
21
19
|
Arguments:
|
|
22
20
|
cert (Certificate): The certificate to use for cryptographic operations.
|
|
23
21
|
"""
|
|
24
|
-
#
|
|
25
|
-
|
|
22
|
+
# Extract the public key and private key data from the certificate
|
|
23
|
+
private_key = RSA.import_key(cert.private_key())
|
|
24
|
+
public_key = RSA.import_key(cert.public_key())
|
|
26
25
|
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
26
|
+
# Create a new signer from the private key and a new verifier from the public key
|
|
27
|
+
self._signer = pkcs1_15.new(private_key)
|
|
28
|
+
self._verifier = pkcs1_15.new(public_key)
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self._private_key = private_key
|
|
35
|
-
else:
|
|
36
|
-
raise TypeError(f"Private key of type ({type(private_key).__name__}) was not expected.")
|
|
30
|
+
def verify(self, content: bytes, signature: bytes) -> bool:
|
|
31
|
+
"""Verify a signature against the given content using the certificate.
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
Arguments:
|
|
34
|
+
content (bytes): The content to verify.
|
|
35
|
+
signature (bytes): The signature to verify against the content.
|
|
36
|
+
|
|
37
|
+
Returns: True if the signature is valid, False otherwise.
|
|
38
|
+
"""
|
|
39
|
+
# Hash the content using SHA256
|
|
40
|
+
hashed = SHA256.new(content)
|
|
41
|
+
|
|
42
|
+
# Verify the signature using the public key. This will raise a ValueError if the signature is invalid.
|
|
43
|
+
# We catch this exception and return False to indicate that the signature is invalid.
|
|
44
|
+
try:
|
|
45
|
+
self._verifier.verify(hashed, b64decode(signature))
|
|
46
|
+
return True
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
41
49
|
|
|
42
50
|
def sign(self, data: bytes) -> bytes:
|
|
43
51
|
"""Create a signature from the given data using the certificate.
|
|
@@ -48,10 +56,10 @@ class CryptoWrapper:
|
|
|
48
56
|
Returns: A base64-encoded string containing the signature.
|
|
49
57
|
"""
|
|
50
58
|
# First, hash the data using SHA256
|
|
51
|
-
hashed =
|
|
59
|
+
hashed = SHA256.new(data)
|
|
52
60
|
|
|
53
61
|
# Next, sign the hash using the private key
|
|
54
|
-
signature = self.
|
|
62
|
+
signature = self._signer.sign(hashed)
|
|
55
63
|
|
|
56
64
|
# Finally, return the base64-encoded signature
|
|
57
65
|
return b64encode(signature)
|
mms_client/services/base.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Contains the client layer for communicating with the MMS server."""
|
|
2
2
|
|
|
3
|
+
from base64 import b64decode
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from logging import getLogger
|
|
5
6
|
from typing import Dict
|
|
@@ -52,26 +53,33 @@ class ServiceConfiguration:
|
|
|
52
53
|
|
|
53
54
|
|
|
54
55
|
@dataclass
|
|
55
|
-
class EndpointConfiguration(Generic[E, P]):
|
|
56
|
+
class EndpointConfiguration(Generic[E, P]): # pylint: disable=too-many-instance-attributes
|
|
56
57
|
"""Configuration for an endpoint on the MMS server."""
|
|
57
58
|
|
|
58
59
|
# The name of the endpoint
|
|
59
60
|
name: str
|
|
60
61
|
|
|
61
|
-
# The allowed client types for the endpoint
|
|
62
|
-
allowed_clients: Optional[List[ClientType]]
|
|
63
|
-
|
|
64
62
|
# The service for the endpoint
|
|
65
63
|
service: ServiceConfiguration
|
|
66
64
|
|
|
67
65
|
# The type of request to submit to the MMS server
|
|
68
66
|
request_type: RequestType
|
|
69
67
|
|
|
68
|
+
# The allowed client types for the endpoint
|
|
69
|
+
allowed_clients: Optional[List[ClientType]] = None
|
|
70
|
+
|
|
70
71
|
# The type of payload to expect in the response
|
|
71
|
-
response_envelope_type: Optional[Type[E]]
|
|
72
|
+
response_envelope_type: Optional[Type[E]] = None
|
|
72
73
|
|
|
73
74
|
# The type of data to expect in the response
|
|
74
|
-
response_data_type: Optional[Type[P]]
|
|
75
|
+
response_data_type: Optional[Type[P]] = None
|
|
76
|
+
|
|
77
|
+
# Whether the endpoint is for a report request
|
|
78
|
+
for_report: bool = False
|
|
79
|
+
|
|
80
|
+
# An optional serializer used to deserialize the response data for the service. This is only used for report
|
|
81
|
+
# requests because they have a separate XSD file for responses.
|
|
82
|
+
serializer: Optional[Serializer] = None
|
|
75
83
|
|
|
76
84
|
|
|
77
85
|
class ClientProto(Protocol):
|
|
@@ -85,6 +93,10 @@ class ClientProto(Protocol):
|
|
|
85
93
|
def user(self) -> str:
|
|
86
94
|
"""Return the user name of the person making the request."""
|
|
87
95
|
|
|
96
|
+
@property
|
|
97
|
+
def client_type(self) -> ClientType:
|
|
98
|
+
"""Return the type of client to use for making requests to the MMS server."""
|
|
99
|
+
|
|
88
100
|
def verify_audience(self, config: EndpointConfiguration) -> None:
|
|
89
101
|
"""Verify that the client type is allowed.
|
|
90
102
|
|
|
@@ -131,14 +143,7 @@ class ClientProto(Protocol):
|
|
|
131
143
|
"""
|
|
132
144
|
|
|
133
145
|
|
|
134
|
-
def mms_endpoint(
|
|
135
|
-
name: str,
|
|
136
|
-
service: ServiceConfiguration,
|
|
137
|
-
request_type: RequestType,
|
|
138
|
-
allowed_clients: Optional[List[ClientType]] = None,
|
|
139
|
-
resp_envelope_type: Optional[Type[E]] = None,
|
|
140
|
-
resp_data_type: Optional[Type[P]] = None,
|
|
141
|
-
):
|
|
146
|
+
def mms_endpoint(**kwargs):
|
|
142
147
|
"""Create a decorator for an MMS endpoint.
|
|
143
148
|
|
|
144
149
|
This decorator is used to mark a method as an MMS endpoint. It will add the endpoint configuration to the function
|
|
@@ -151,13 +156,16 @@ def mms_endpoint(
|
|
|
151
156
|
request_type (RequestType): The type of request to submit to the MMS server.
|
|
152
157
|
allowed_clients (List[ClientType]): The types of clients that are allowed to access the endpoint. If this is not
|
|
153
158
|
provided, then any client will be allowed.
|
|
154
|
-
|
|
159
|
+
response_envelope_type (Type[E]): The type of payload to expect in the response. If this is not provided, then the
|
|
155
160
|
response envelope will be assumed to have the same type as the request envelope.
|
|
156
|
-
|
|
161
|
+
response_data_type (Type[P]): The type of data to expect in the response. If this is not provided, then the
|
|
157
162
|
response data will be assumed to have the same type as the request data.
|
|
163
|
+
for_report (bool): If True, the endpoint is for a report request.
|
|
164
|
+
serializer (Serializer): The serializer to use for responses from the endpoint. Overrides the default
|
|
165
|
+
serializer for the service.
|
|
158
166
|
"""
|
|
159
167
|
# First, create the endpoint configuration from the given parameters
|
|
160
|
-
config = EndpointConfiguration(
|
|
168
|
+
config = EndpointConfiguration(**kwargs)
|
|
161
169
|
|
|
162
170
|
# Next, create a decorator that will add the endpoint configuration to the function
|
|
163
171
|
def decorator(func):
|
|
@@ -168,10 +176,18 @@ def mms_endpoint(
|
|
|
168
176
|
self.verify_audience(config)
|
|
169
177
|
|
|
170
178
|
# Next, call the wrapped function to get the envelope
|
|
171
|
-
|
|
179
|
+
result = func(self, *args, **kwargs)
|
|
180
|
+
if isinstance(result, tuple):
|
|
181
|
+
envelope, callback = result
|
|
182
|
+
else:
|
|
183
|
+
envelope, callback = result, None
|
|
172
184
|
|
|
173
185
|
# Now, submit the request to the MMS server and get the response
|
|
174
|
-
resp,
|
|
186
|
+
resp, attachments = self.request_one(envelope, args[0], config)
|
|
187
|
+
|
|
188
|
+
# Call the callback function if it was provided
|
|
189
|
+
if callback:
|
|
190
|
+
callback(resp, attachments)
|
|
175
191
|
|
|
176
192
|
# Finally, extract the data from the response and return it
|
|
177
193
|
logger.info(f"{config.name}: Returning {type(resp.data).__name__} data.")
|
|
@@ -183,14 +199,7 @@ def mms_endpoint(
|
|
|
183
199
|
return decorator
|
|
184
200
|
|
|
185
201
|
|
|
186
|
-
def mms_multi_endpoint(
|
|
187
|
-
name: str,
|
|
188
|
-
service: ServiceConfiguration,
|
|
189
|
-
request_type: RequestType,
|
|
190
|
-
allowed_clients: Optional[List[ClientType]] = None,
|
|
191
|
-
resp_envelope_type: Optional[Type[E]] = None,
|
|
192
|
-
resp_data_type: Optional[Type[P]] = None,
|
|
193
|
-
):
|
|
202
|
+
def mms_multi_endpoint(**kwargs):
|
|
194
203
|
"""Create a decorator for an MMS multi-response endpoint.
|
|
195
204
|
|
|
196
205
|
This decorator is used to mark a method as an MMS multi-response endpoint. It will add the endpoint configuration to
|
|
@@ -204,17 +213,20 @@ def mms_multi_endpoint(
|
|
|
204
213
|
request_type (RequestType): The type of request to submit to the MMS server.
|
|
205
214
|
allowed_clients (List[ClientType]): The types of clients that are allowed to access the endpoint. If this is not
|
|
206
215
|
provided, then any client will be allowed.
|
|
207
|
-
|
|
216
|
+
response_envelope_type (Type[E]): The type of payload to expect in the response. If this is not provided, then
|
|
208
217
|
the response envelope will be assumed to have the same type as the request
|
|
209
218
|
envelope.
|
|
210
|
-
|
|
219
|
+
response_data_type (Type[P]): The type of data to expect in the response. If this is not provided, then the
|
|
211
220
|
response data will be assumed to have the same type as the request data. Note,
|
|
212
221
|
that this is not intended to account for the expected sequence type of the
|
|
213
222
|
response data. That is already handled in the wrapped function, so this should
|
|
214
223
|
only be set if the inner data type being returned differs from what was sent.
|
|
224
|
+
for_report (bool): If True, the endpoint is for a report request.
|
|
225
|
+
serializer (Serializer): The serializer to use for responses from the endpoint. Overrides the default
|
|
226
|
+
serializer for the service.
|
|
215
227
|
"""
|
|
216
228
|
# First, create the endpoint configuration from the given parameters
|
|
217
|
-
config = EndpointConfiguration(
|
|
229
|
+
config = EndpointConfiguration(**kwargs)
|
|
218
230
|
|
|
219
231
|
# Next, create a decorator that will add the endpoint configuration to the function
|
|
220
232
|
def decorator(func):
|
|
@@ -225,10 +237,18 @@ def mms_multi_endpoint(
|
|
|
225
237
|
self.verify_audience(config)
|
|
226
238
|
|
|
227
239
|
# Next, call the wrapped function to get the envelope
|
|
228
|
-
|
|
240
|
+
result = func(self, *args, **kwargs)
|
|
241
|
+
if isinstance(result, tuple):
|
|
242
|
+
envelope, callback = result # pragma: no cover
|
|
243
|
+
else:
|
|
244
|
+
envelope, callback = result, None
|
|
229
245
|
|
|
230
246
|
# Now, submit the request to the MMS server and get the response
|
|
231
|
-
resp,
|
|
247
|
+
resp, attachments = self.request_many(envelope, args[0], config)
|
|
248
|
+
|
|
249
|
+
# Call the callback function if it was provided
|
|
250
|
+
if callback:
|
|
251
|
+
callback(resp, attachments) # pragma: no cover
|
|
232
252
|
|
|
233
253
|
# Finally, extract the data from the response and return it
|
|
234
254
|
logger.info(f"{config.name}: Returning {len(resp.data)} item(s).")
|
|
@@ -248,6 +268,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
248
268
|
|
|
249
269
|
def __init__(
|
|
250
270
|
self,
|
|
271
|
+
domain: str,
|
|
251
272
|
participant: str,
|
|
252
273
|
user: str,
|
|
253
274
|
client_type: ClientType,
|
|
@@ -259,6 +280,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
259
280
|
"""Create a new MMS client with the given participant, user, client type, and authentication.
|
|
260
281
|
|
|
261
282
|
Arguments:
|
|
283
|
+
domain (str): The domain to use when signing the content ID to MTOM attachments.
|
|
262
284
|
participant (str): The MMS code of the business entity to which the requesting user belongs.
|
|
263
285
|
user (str): The user name of the person making the request.
|
|
264
286
|
client_type (ClientType): The type of client to use for making requests to the MMS server.
|
|
@@ -269,6 +291,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
269
291
|
test (bool): Whether to use the test server.
|
|
270
292
|
"""
|
|
271
293
|
# First, save the base field associated with the client
|
|
294
|
+
self._domain = domain
|
|
272
295
|
self._participant = participant
|
|
273
296
|
self._user = user
|
|
274
297
|
self._client_type = client_type
|
|
@@ -295,6 +318,11 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
295
318
|
"""Return the user name of the person making the request."""
|
|
296
319
|
return self._user
|
|
297
320
|
|
|
321
|
+
@property
|
|
322
|
+
def client_type(self) -> ClientType:
|
|
323
|
+
"""Return the type of client to use for making requests to the MMS server."""
|
|
324
|
+
return self._client_type
|
|
325
|
+
|
|
298
326
|
def verify_audience(self, config: EndpointConfiguration) -> None:
|
|
299
327
|
"""Verify that the client type is allowed.
|
|
300
328
|
|
|
@@ -329,23 +357,29 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
329
357
|
|
|
330
358
|
Returns: The response from the MMS server.
|
|
331
359
|
"""
|
|
360
|
+
# Create a new ZWrapper for the given service
|
|
361
|
+
wrapper = self._get_wrapper(config.service)
|
|
362
|
+
|
|
332
363
|
# First, create the MMS request from the payload and data.
|
|
333
364
|
logger.debug(
|
|
334
365
|
f"{config.name}: Starting request. Envelope: {type(envelope).__name__}, Data: {type(payload).__name__}",
|
|
335
366
|
)
|
|
336
|
-
request = self._to_mms_request(
|
|
367
|
+
request = self._to_mms_request(
|
|
368
|
+
wrapper, config.request_type, config.service.serializer.serialize(envelope, payload, config.for_report)
|
|
369
|
+
)
|
|
337
370
|
|
|
338
371
|
# Next, submit the request to the MMS server and get and verify the response.
|
|
339
|
-
resp =
|
|
372
|
+
resp = wrapper.submit(request)
|
|
340
373
|
self._verify_mms_response(resp, config)
|
|
341
374
|
|
|
342
375
|
# Now, extract the attachments from the response
|
|
343
|
-
attachments = {a.name: a.data for a in resp.attachments}
|
|
376
|
+
attachments = {a.name: b64decode(a.data) for a in resp.attachments}
|
|
344
377
|
|
|
345
378
|
# Finally, deserialize and verify the response
|
|
346
379
|
envelope_type = config.response_envelope_type or type(envelope)
|
|
347
380
|
data_type = config.response_data_type or type(payload)
|
|
348
|
-
|
|
381
|
+
deserializer = config.serializer or config.service.serializer
|
|
382
|
+
data: Response[E, P] = deserializer.deserialize(resp.payload, envelope_type, data_type, config.for_report)
|
|
349
383
|
self._verify_response(data, config)
|
|
350
384
|
|
|
351
385
|
# Return the response data and any attachments
|
|
@@ -369,6 +403,9 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
369
403
|
|
|
370
404
|
Returns: The multi-response from the MMS server.
|
|
371
405
|
"""
|
|
406
|
+
# Create a new ZWrapper for the given service
|
|
407
|
+
wrapper = self._get_wrapper(config.service)
|
|
408
|
+
|
|
372
409
|
# First, create the MMS request from the payload and data.
|
|
373
410
|
is_list = isinstance(payload, list)
|
|
374
411
|
data_type = type(payload[0]) if is_list else type(payload) # type: ignore[index]
|
|
@@ -379,26 +416,30 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
379
416
|
),
|
|
380
417
|
)
|
|
381
418
|
serialized = (
|
|
382
|
-
config.service.serializer.serialize_multi(
|
|
419
|
+
config.service.serializer.serialize_multi(
|
|
420
|
+
envelope, payload, data_type, config.for_report # type: ignore[arg-type]
|
|
421
|
+
)
|
|
383
422
|
if is_list
|
|
384
|
-
else config.service.serializer.serialize(envelope, payload) # type: ignore[type-var]
|
|
423
|
+
else config.service.serializer.serialize(envelope, payload, config.for_report) # type: ignore[type-var]
|
|
385
424
|
)
|
|
386
|
-
request = self._to_mms_request(config.request_type, serialized)
|
|
425
|
+
request = self._to_mms_request(wrapper, config.request_type, serialized)
|
|
387
426
|
|
|
388
427
|
# Next, submit the request to the MMS server and get and verify the response.
|
|
389
|
-
resp =
|
|
428
|
+
resp = wrapper.submit(request)
|
|
390
429
|
self._verify_mms_response(resp, config)
|
|
391
430
|
|
|
392
431
|
# Now, extract the attachments from the response
|
|
393
|
-
attachments = {a.name: a.data for a in resp.attachments}
|
|
432
|
+
attachments = {a.name: b64decode(a.data) for a in resp.attachments}
|
|
394
433
|
|
|
395
434
|
# Finally, deserialize and verify the response
|
|
396
435
|
envelope_type = config.response_envelope_type or type(envelope)
|
|
397
436
|
data_type = config.response_data_type or data_type
|
|
398
|
-
|
|
437
|
+
deserializer = config.serializer or config.service.serializer
|
|
438
|
+
data: MultiResponse[E, P] = deserializer.deserialize_multi(
|
|
399
439
|
resp.payload,
|
|
400
440
|
envelope_type,
|
|
401
441
|
data_type, # type: ignore[arg-type]
|
|
442
|
+
config.for_report,
|
|
402
443
|
)
|
|
403
444
|
self._verify_multi_response(data, config)
|
|
404
445
|
|
|
@@ -410,6 +451,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
410
451
|
|
|
411
452
|
def _to_mms_request(
|
|
412
453
|
self,
|
|
454
|
+
client: ZWrapper,
|
|
413
455
|
req_type: RequestType,
|
|
414
456
|
data: bytes,
|
|
415
457
|
return_req: bool = False,
|
|
@@ -418,6 +460,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
418
460
|
"""Convert the given data to an MMS request.
|
|
419
461
|
|
|
420
462
|
Arguments:
|
|
463
|
+
client (ZWrapper): The Zeep client to use for submitting the request.
|
|
421
464
|
req_type (RequestType): The type of request to submit to the MMS server.
|
|
422
465
|
data (bytes): The data to submit to the MMS server.
|
|
423
466
|
return_req (bool): Whether to return the request data in the response. This is False by default.
|
|
@@ -425,16 +468,14 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
425
468
|
|
|
426
469
|
Arguments: The MMS request to submit to the MMS server.
|
|
427
470
|
"""
|
|
428
|
-
#
|
|
471
|
+
# First, convert the attachments to the correct the MMS format
|
|
429
472
|
attachment_data = (
|
|
430
|
-
[
|
|
431
|
-
Attachment(signature=self._signer.sign(data), name=name, binaryData=data)
|
|
432
|
-
for name, data in attachments.items()
|
|
433
|
-
]
|
|
434
|
-
if attachments
|
|
435
|
-
else []
|
|
473
|
+
[self._to_mms_attachment(client, name, data) for name, data in attachments.items()] if attachments else []
|
|
436
474
|
)
|
|
437
475
|
|
|
476
|
+
# Next, convert the payload to a base-64 string
|
|
477
|
+
tag, signature = self._register_and_sign(client, "payload", data)
|
|
478
|
+
|
|
438
479
|
# Embed the data and the attachments in the MMS request and return it
|
|
439
480
|
logger.debug(
|
|
440
481
|
f"Creating MMS request of type {req_type.name} to send {len(data)} bytes of data and "
|
|
@@ -445,11 +486,36 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
445
486
|
adminRole=self._is_admin,
|
|
446
487
|
requestDataType=RequestDataType.XML,
|
|
447
488
|
sendRequestDataOnSuccess=return_req,
|
|
448
|
-
requestSignature=
|
|
449
|
-
requestData=
|
|
489
|
+
requestSignature=signature,
|
|
490
|
+
requestData=tag,
|
|
450
491
|
attachmentData=attachment_data,
|
|
451
492
|
)
|
|
452
493
|
|
|
494
|
+
def _to_mms_attachment(self, client: ZWrapper, name: str, data: bytes) -> Attachment: # pragma: no cover
|
|
495
|
+
"""Convert the given data to an MMS attachment.
|
|
496
|
+
|
|
497
|
+
Arguments:
|
|
498
|
+
client (ZWrapper): The Zeep client to use for submitting the request.
|
|
499
|
+
name (str): The name of the attachment.
|
|
500
|
+
data (bytes): The data to be attached.
|
|
501
|
+
|
|
502
|
+
Returns: The MMS attachment.
|
|
503
|
+
"""
|
|
504
|
+
# Convert the data to a base-64 string
|
|
505
|
+
tag, signature = self._register_and_sign(client, name, data)
|
|
506
|
+
|
|
507
|
+
# Create the MMS attachment and return it
|
|
508
|
+
return Attachment(signature=signature, name=name, binaryData=tag)
|
|
509
|
+
|
|
510
|
+
def _register_and_sign(self, client: ZWrapper, name: str, data: bytes) -> Tuple[str, str]:
|
|
511
|
+
tag = client.register_attachment(name, data)
|
|
512
|
+
|
|
513
|
+
# Next, sign the data
|
|
514
|
+
signature = self._signer.sign(data)
|
|
515
|
+
|
|
516
|
+
# Finally, convert the encoded data to a string and return it and the signature
|
|
517
|
+
return tag, signature.decode("UTF-8")
|
|
518
|
+
|
|
453
519
|
def _verify_mms_response(self, resp: MmsResponse, config: EndpointConfiguration) -> None:
|
|
454
520
|
"""Verify that the given MMS response is valid.
|
|
455
521
|
|
|
@@ -537,18 +603,18 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
537
603
|
Returns: True to indicate that the response is valid, False otherwise.
|
|
538
604
|
"""
|
|
539
605
|
# Log the request's processing statistics
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
606
|
+
if resp.statistics is not None:
|
|
607
|
+
logger.info(
|
|
608
|
+
f"{config.name} ({resp.statistics.timestamp_xml}): Recieved {resp.statistics.received}, "
|
|
609
|
+
f"Valid: {resp.statistics.valid}, Invalid: {resp.statistics.invalid}, "
|
|
610
|
+
f"Successful: {resp.statistics.successful}, Unsuccessful: {resp.statistics.unsuccessful} "
|
|
611
|
+
f"in {resp.statistics.time_ms}ms"
|
|
612
|
+
)
|
|
546
613
|
|
|
547
614
|
# Check if the response is invalid and if the envelope had any validation issues. If not, then we have a
|
|
548
615
|
# valid base response so return True. Otherwise, return False.
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
)
|
|
616
|
+
is_invalid = resp.statistics is not None and resp.statistics.invalid
|
|
617
|
+
return not is_invalid and self._verify_response_common(config, type(resp.envelope), resp.envelope_validation)
|
|
552
618
|
|
|
553
619
|
def _verify_messages(self, config: EndpointConfiguration, resp: BaseResponse[E]) -> None:
|
|
554
620
|
"""Verify the messages in the given response.
|
|
@@ -558,12 +624,12 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
558
624
|
resp (BaseResponse): The response to verify.
|
|
559
625
|
"""
|
|
560
626
|
for path, messages in resp.messages.items():
|
|
561
|
-
for info in messages.information:
|
|
562
|
-
logger.info(f"{config.name} - {path}: {info
|
|
563
|
-
for warning in messages.warnings:
|
|
564
|
-
logger.warning(f"{config.name} - {path}: {warning
|
|
565
|
-
for error in messages.errors:
|
|
566
|
-
logger.error(f"{config.name} - {path}: {error
|
|
627
|
+
for info in messages.information: # type: ignore[union-attr]
|
|
628
|
+
logger.info(f"{config.name} - {path}: {info}")
|
|
629
|
+
for warning in messages.warnings: # type: ignore[union-attr]
|
|
630
|
+
logger.warning(f"{config.name} - {path}: {warning}")
|
|
631
|
+
for error in messages.errors: # type: ignore[union-attr]
|
|
632
|
+
logger.error(f"{config.name} - {path}: {error}")
|
|
567
633
|
|
|
568
634
|
def _verify_response_common(
|
|
569
635
|
self, config: EndpointConfiguration, payload_type: type, resp: ResponseCommon, index: Optional[int] = None
|
|
@@ -597,6 +663,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
597
663
|
if service.interface not in self._wrappers:
|
|
598
664
|
logger.debug(f"Creating wrapper for {service.interface.name} interface.")
|
|
599
665
|
self._wrappers[service.interface] = ZWrapper(
|
|
666
|
+
self._domain,
|
|
600
667
|
self._client_type,
|
|
601
668
|
service.interface,
|
|
602
669
|
self._cert.to_adapter(),
|
mms_client/services/market.py
CHANGED
|
@@ -37,14 +37,14 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
37
37
|
config = ServiceConfiguration(Interface.MI, Serializer(SchemaType.MARKET, "MarketData"))
|
|
38
38
|
|
|
39
39
|
@mms_endpoint(
|
|
40
|
-
"MarketQuery_ReserveRequirementQuery",
|
|
41
|
-
config,
|
|
42
|
-
RequestType.INFO,
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
name="MarketQuery_ReserveRequirementQuery",
|
|
41
|
+
service=config,
|
|
42
|
+
request_type=RequestType.INFO,
|
|
43
|
+
response_envelope_type=MarketSubmit,
|
|
44
|
+
response_data_type=ReserveRequirement,
|
|
45
45
|
)
|
|
46
46
|
def query_reserve_requirements(
|
|
47
|
-
self: ClientProto, request: ReserveRequirementQuery, date: Optional[Date] = None
|
|
47
|
+
self: ClientProto, request: ReserveRequirementQuery, days: int, date: Optional[Date] = None
|
|
48
48
|
) -> List[ReserveRequirement]:
|
|
49
49
|
"""Query the MMS server for reserve requirements.
|
|
50
50
|
|
|
@@ -54,6 +54,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
54
54
|
request (ReserveRequirementQuery): The query to submit to the MMS server.
|
|
55
55
|
date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults
|
|
56
56
|
to the current date.
|
|
57
|
+
days (int): The number of days ahead for which the data is being queried.
|
|
57
58
|
|
|
58
59
|
Returns: A list of reserve requirements that match the query.
|
|
59
60
|
"""
|
|
@@ -62,10 +63,12 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
62
63
|
date=date or Date.today(),
|
|
63
64
|
participant=self.participant,
|
|
64
65
|
user=self.user,
|
|
65
|
-
days=
|
|
66
|
+
days=days,
|
|
66
67
|
)
|
|
67
68
|
|
|
68
|
-
@mms_endpoint(
|
|
69
|
+
@mms_endpoint(
|
|
70
|
+
name="MarketSubmit_OfferData", service=config, request_type=RequestType.MARKET, allowed_clients=[ClientType.BSP]
|
|
71
|
+
)
|
|
69
72
|
def put_offer(
|
|
70
73
|
self: ClientProto, request: OfferData, market_type: MarketType, days: int, date: Optional[Date] = None
|
|
71
74
|
) -> OfferData:
|
|
@@ -91,7 +94,9 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
91
94
|
days=days,
|
|
92
95
|
)
|
|
93
96
|
|
|
94
|
-
@mms_multi_endpoint(
|
|
97
|
+
@mms_multi_endpoint(
|
|
98
|
+
name="MarketSubmit_OfferData", service=config, request_type=RequestType.MARKET, allowed_clients=[ClientType.BSP]
|
|
99
|
+
)
|
|
95
100
|
def put_offers(
|
|
96
101
|
self: ClientProto, requests: List[OfferData], market_type: MarketType, days: int, date: Optional[Date] = None
|
|
97
102
|
) -> List[OfferData]:
|
|
@@ -118,7 +123,11 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
118
123
|
)
|
|
119
124
|
|
|
120
125
|
@mms_multi_endpoint(
|
|
121
|
-
"MarketQuery_OfferQuery",
|
|
126
|
+
name="MarketQuery_OfferQuery",
|
|
127
|
+
service=config,
|
|
128
|
+
request_type=RequestType.MARKET,
|
|
129
|
+
response_envelope_type=MarketSubmit,
|
|
130
|
+
response_data_type=OfferData,
|
|
122
131
|
)
|
|
123
132
|
def query_offers(self: ClientProto, request: OfferQuery, days: int, date: Optional[Date] = None) -> List[OfferData]:
|
|
124
133
|
"""Query the MMS server for offers.
|
|
@@ -141,7 +150,12 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
141
150
|
days=days,
|
|
142
151
|
)
|
|
143
152
|
|
|
144
|
-
@mms_endpoint(
|
|
153
|
+
@mms_endpoint(
|
|
154
|
+
name="MarketCancel_OfferCancel",
|
|
155
|
+
service=config,
|
|
156
|
+
request_type=RequestType.MARKET,
|
|
157
|
+
allowed_clients=[ClientType.BSP],
|
|
158
|
+
)
|
|
145
159
|
def cancel_offer(
|
|
146
160
|
self: ClientProto, request: OfferCancel, market_type: MarketType, days: int, date: Optional[Date] = None
|
|
147
161
|
) -> OfferCancel:
|
|
@@ -167,7 +181,12 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
167
181
|
days=days,
|
|
168
182
|
)
|
|
169
183
|
|
|
170
|
-
@mms_endpoint(
|
|
184
|
+
@mms_endpoint(
|
|
185
|
+
name="MarketQuery_AwardResultsQuery",
|
|
186
|
+
service=config,
|
|
187
|
+
request_type=RequestType.MARKET,
|
|
188
|
+
response_data_type=AwardResponse,
|
|
189
|
+
)
|
|
171
190
|
def query_awards(self: ClientProto, request: AwardQuery, days: int, date: Optional[Date] = None) -> AwardResponse:
|
|
172
191
|
"""Query the MMS server for award results.
|
|
173
192
|
|