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.
@@ -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)
@@ -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 cryptography.hazmat.backends import default_backend
7
- from cryptography.hazmat.primitives import hashes
8
- from cryptography.hazmat.primitives.asymmetric import padding
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
- # First, save our certificate for later use
25
- self._cert = cert
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
- # Next, import the private key from the certificate
28
- private_key, _, _ = load_key_and_certificates(
29
- self._cert.certificate, self._cert.passphrase.encode(), default_backend()
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
- # Now, we need to assert typing on this private key to make mypy happy
33
- if isinstance(private_key, RSAPrivateKey):
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
- # Finally, save our padding and algorithm for later use
39
- self._padding = padding.PKCS1v15()
40
- self._algorithm = hashes.SHA256()
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 = sha256(data)
59
+ hashed = SHA256.new(data)
52
60
 
53
61
  # Next, sign the hash using the private key
54
- signature = self._private_key.sign(hashed.digest(), self._padding, self._algorithm)
62
+ signature = self._signer.sign(hashed)
55
63
 
56
64
  # Finally, return the base64-encoded signature
57
65
  return b64encode(signature)
@@ -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
- resp_envelope_type (Type[E]): The type of payload to expect in the response. If this is not provided, then the
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
- resp_data_type (Type[P]): The type of data to expect in the response. If this is not provided, then the
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(name, allowed_clients, service, request_type, resp_envelope_type, resp_data_type)
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
- envelope = func(self, *args, **kwargs)
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, _ = self.request_one(envelope, args[0], config)
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
- resp_envelope_type (Type[E]): The type of payload to expect in the response. If this is not provided, then
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
- resp_data_type (Type[P]): The type of data to expect in the response. If this is not provided, then the
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(name, allowed_clients, service, request_type, resp_envelope_type, resp_data_type)
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
- envelope = func(self, *args, **kwargs)
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, _ = self.request_many(envelope, args[0], config)
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(config.request_type, config.service.serializer.serialize(envelope, payload))
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 = self._get_wrapper(config.service).submit(request)
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
- data: Response[E, P] = config.service.serializer.deserialize(resp.payload, envelope_type, data_type)
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(envelope, payload, data_type) # type: ignore[arg-type]
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 = self._get_wrapper(config.service).submit(request)
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
- data: MultiResponse[E, P] = config.service.serializer.deserialize_multi(
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
- # Convert the attachments to the correct the MMS format
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=self._signer.sign(data),
449
- requestData=data,
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
- logger.info(
541
- f"{config.name} ({resp.statistics.timestamp_xml}): Recieved {resp.statistics.received}, "
542
- f"Valid: {resp.statistics.valid}, Invalid: {resp.statistics.invalid}, "
543
- f"Successful: {resp.statistics.successful}, Unsuccessful: {resp.statistics.unsuccessful} "
544
- f"in {resp.statistics.time_ms}ms"
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
- return resp.statistics.invalid == 0 and self._verify_response_common(
550
- config, type(resp.envelope), resp.envelope_validation
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.code}")
563
- for warning in messages.warnings:
564
- logger.warning(f"{config.name} - {path}: {warning.code}")
565
- for error in messages.errors:
566
- logger.error(f"{config.name} - {path}: {error.code}")
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(),
@@ -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
- resp_envelope_type=MarketSubmit,
44
- resp_data_type=ReserveRequirement,
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=1,
66
+ days=days,
66
67
  )
67
68
 
68
- @mms_endpoint("MarketSubmit_OfferData", config, RequestType.INFO, [ClientType.BSP])
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("MarketSubmit_OfferData", config, RequestType.INFO, [ClientType.BSP])
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", config, RequestType.INFO, resp_envelope_type=MarketSubmit, resp_data_type=OfferData
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("MarketCancel_OfferCancel", config, RequestType.INFO, [ClientType.BSP])
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("MarketQuery_AwardResultsQuery", config, RequestType.INFO, resp_data_type=AwardResponse)
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