mms-client 1.6.0__py3-none-any.whl → 1.7.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 +53 -15
- mms_client/services/market.py +4 -3
- mms_client/types/base.py +1 -4
- mms_client/types/transport.py +2 -2
- mms_client/utils/multipart_transport.py +259 -0
- mms_client/utils/serialization.py +26 -2
- mms_client/utils/web.py +16 -2
- {mms_client-1.6.0.dist-info → mms_client-1.7.0.dist-info}/METADATA +9 -5
- {mms_client-1.6.0.dist-info → mms_client-1.7.0.dist-info}/RECORD +13 -12
- {mms_client-1.6.0.dist-info → mms_client-1.7.0.dist-info}/LICENSE +0 -0
- {mms_client-1.6.0.dist-info → mms_client-1.7.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
|
|
@@ -248,6 +249,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
248
249
|
|
|
249
250
|
def __init__(
|
|
250
251
|
self,
|
|
252
|
+
domain: str,
|
|
251
253
|
participant: str,
|
|
252
254
|
user: str,
|
|
253
255
|
client_type: ClientType,
|
|
@@ -259,6 +261,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
259
261
|
"""Create a new MMS client with the given participant, user, client type, and authentication.
|
|
260
262
|
|
|
261
263
|
Arguments:
|
|
264
|
+
domain (str): The domain to use when signing the content ID to MTOM attachments.
|
|
262
265
|
participant (str): The MMS code of the business entity to which the requesting user belongs.
|
|
263
266
|
user (str): The user name of the person making the request.
|
|
264
267
|
client_type (ClientType): The type of client to use for making requests to the MMS server.
|
|
@@ -269,6 +272,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
269
272
|
test (bool): Whether to use the test server.
|
|
270
273
|
"""
|
|
271
274
|
# First, save the base field associated with the client
|
|
275
|
+
self._domain = domain
|
|
272
276
|
self._participant = participant
|
|
273
277
|
self._user = user
|
|
274
278
|
self._client_type = client_type
|
|
@@ -329,18 +333,23 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
329
333
|
|
|
330
334
|
Returns: The response from the MMS server.
|
|
331
335
|
"""
|
|
336
|
+
# Create a new ZWrapper for the given service
|
|
337
|
+
wrapper = self._get_wrapper(config.service)
|
|
338
|
+
|
|
332
339
|
# First, create the MMS request from the payload and data.
|
|
333
340
|
logger.debug(
|
|
334
341
|
f"{config.name}: Starting request. Envelope: {type(envelope).__name__}, Data: {type(payload).__name__}",
|
|
335
342
|
)
|
|
336
|
-
request = self._to_mms_request(
|
|
343
|
+
request = self._to_mms_request(
|
|
344
|
+
wrapper, config.request_type, config.service.serializer.serialize(envelope, payload)
|
|
345
|
+
)
|
|
337
346
|
|
|
338
347
|
# Next, submit the request to the MMS server and get and verify the response.
|
|
339
|
-
resp =
|
|
348
|
+
resp = wrapper.submit(request)
|
|
340
349
|
self._verify_mms_response(resp, config)
|
|
341
350
|
|
|
342
351
|
# Now, extract the attachments from the response
|
|
343
|
-
attachments = {a.name: a.data for a in resp.attachments}
|
|
352
|
+
attachments = {a.name: b64decode(a.data) for a in resp.attachments}
|
|
344
353
|
|
|
345
354
|
# Finally, deserialize and verify the response
|
|
346
355
|
envelope_type = config.response_envelope_type or type(envelope)
|
|
@@ -369,6 +378,9 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
369
378
|
|
|
370
379
|
Returns: The multi-response from the MMS server.
|
|
371
380
|
"""
|
|
381
|
+
# Create a new ZWrapper for the given service
|
|
382
|
+
wrapper = self._get_wrapper(config.service)
|
|
383
|
+
|
|
372
384
|
# First, create the MMS request from the payload and data.
|
|
373
385
|
is_list = isinstance(payload, list)
|
|
374
386
|
data_type = type(payload[0]) if is_list else type(payload) # type: ignore[index]
|
|
@@ -383,14 +395,14 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
383
395
|
if is_list
|
|
384
396
|
else config.service.serializer.serialize(envelope, payload) # type: ignore[type-var]
|
|
385
397
|
)
|
|
386
|
-
request = self._to_mms_request(config.request_type, serialized)
|
|
398
|
+
request = self._to_mms_request(wrapper, config.request_type, serialized)
|
|
387
399
|
|
|
388
400
|
# Next, submit the request to the MMS server and get and verify the response.
|
|
389
|
-
resp =
|
|
401
|
+
resp = wrapper.submit(request)
|
|
390
402
|
self._verify_mms_response(resp, config)
|
|
391
403
|
|
|
392
404
|
# Now, extract the attachments from the response
|
|
393
|
-
attachments = {a.name: a.data for a in resp.attachments}
|
|
405
|
+
attachments = {a.name: b64decode(a.data) for a in resp.attachments}
|
|
394
406
|
|
|
395
407
|
# Finally, deserialize and verify the response
|
|
396
408
|
envelope_type = config.response_envelope_type or type(envelope)
|
|
@@ -410,6 +422,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
410
422
|
|
|
411
423
|
def _to_mms_request(
|
|
412
424
|
self,
|
|
425
|
+
client: ZWrapper,
|
|
413
426
|
req_type: RequestType,
|
|
414
427
|
data: bytes,
|
|
415
428
|
return_req: bool = False,
|
|
@@ -418,6 +431,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
418
431
|
"""Convert the given data to an MMS request.
|
|
419
432
|
|
|
420
433
|
Arguments:
|
|
434
|
+
client (ZWrapper): The Zeep client to use for submitting the request.
|
|
421
435
|
req_type (RequestType): The type of request to submit to the MMS server.
|
|
422
436
|
data (bytes): The data to submit to the MMS server.
|
|
423
437
|
return_req (bool): Whether to return the request data in the response. This is False by default.
|
|
@@ -425,16 +439,14 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
425
439
|
|
|
426
440
|
Arguments: The MMS request to submit to the MMS server.
|
|
427
441
|
"""
|
|
428
|
-
#
|
|
442
|
+
# First, convert the attachments to the correct the MMS format
|
|
429
443
|
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 []
|
|
444
|
+
[self._to_mms_attachment(client, name, data) for name, data in attachments.items()] if attachments else []
|
|
436
445
|
)
|
|
437
446
|
|
|
447
|
+
# Next, convert the payload to a base-64 string
|
|
448
|
+
tag, signature = self._register_and_sign(client, "payload", data)
|
|
449
|
+
|
|
438
450
|
# Embed the data and the attachments in the MMS request and return it
|
|
439
451
|
logger.debug(
|
|
440
452
|
f"Creating MMS request of type {req_type.name} to send {len(data)} bytes of data and "
|
|
@@ -445,11 +457,36 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
445
457
|
adminRole=self._is_admin,
|
|
446
458
|
requestDataType=RequestDataType.XML,
|
|
447
459
|
sendRequestDataOnSuccess=return_req,
|
|
448
|
-
requestSignature=
|
|
449
|
-
requestData=
|
|
460
|
+
requestSignature=signature,
|
|
461
|
+
requestData=tag,
|
|
450
462
|
attachmentData=attachment_data,
|
|
451
463
|
)
|
|
452
464
|
|
|
465
|
+
def _to_mms_attachment(self, client: ZWrapper, name: str, data: bytes) -> Attachment: # pragma: no cover
|
|
466
|
+
"""Convert the given data to an MMS attachment.
|
|
467
|
+
|
|
468
|
+
Arguments:
|
|
469
|
+
client (ZWrapper): The Zeep client to use for submitting the request.
|
|
470
|
+
name (str): The name of the attachment.
|
|
471
|
+
data (bytes): The data to be attached.
|
|
472
|
+
|
|
473
|
+
Returns: The MMS attachment.
|
|
474
|
+
"""
|
|
475
|
+
# Convert the data to a base-64 string
|
|
476
|
+
tag, signature = self._register_and_sign(client, name, data)
|
|
477
|
+
|
|
478
|
+
# Create the MMS attachment and return it
|
|
479
|
+
return Attachment(signature=signature, name=name, binaryData=tag)
|
|
480
|
+
|
|
481
|
+
def _register_and_sign(self, client: ZWrapper, name: str, data: bytes) -> Tuple[str, str]:
|
|
482
|
+
tag = client.register_attachment(name, data)
|
|
483
|
+
|
|
484
|
+
# Next, sign the data
|
|
485
|
+
signature = self._signer.sign(data)
|
|
486
|
+
|
|
487
|
+
# Finally, convert the encoded data to a string and return it and the signature
|
|
488
|
+
return tag, signature.decode("UTF-8")
|
|
489
|
+
|
|
453
490
|
def _verify_mms_response(self, resp: MmsResponse, config: EndpointConfiguration) -> None:
|
|
454
491
|
"""Verify that the given MMS response is valid.
|
|
455
492
|
|
|
@@ -597,6 +634,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
597
634
|
if service.interface not in self._wrappers:
|
|
598
635
|
logger.debug(f"Creating wrapper for {service.interface.name} interface.")
|
|
599
636
|
self._wrappers[service.interface] = ZWrapper(
|
|
637
|
+
self._domain,
|
|
600
638
|
self._client_type,
|
|
601
639
|
service.interface,
|
|
602
640
|
self._cert.to_adapter(),
|
mms_client/services/market.py
CHANGED
|
@@ -44,7 +44,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
44
44
|
resp_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,7 +63,7 @@ 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
69
|
@mms_endpoint("MarketSubmit_OfferData", config, RequestType.INFO, [ClientType.BSP])
|
|
@@ -167,7 +168,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
167
168
|
days=days,
|
|
168
169
|
)
|
|
169
170
|
|
|
170
|
-
@mms_endpoint("MarketQuery_AwardResultsQuery", config, RequestType.
|
|
171
|
+
@mms_endpoint("MarketQuery_AwardResultsQuery", config, RequestType.MARKET, resp_data_type=AwardResponse)
|
|
171
172
|
def query_awards(self: ClientProto, request: AwardQuery, days: int, date: Optional[Date] = None) -> AwardResponse:
|
|
172
173
|
"""Query the MMS server for award results.
|
|
173
174
|
|
mms_client/types/base.py
CHANGED
|
@@ -115,12 +115,9 @@ class SchemaType(Enum):
|
|
|
115
115
|
OMI = "omi.xsd"
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
class PayloadBase(BaseXmlModel
|
|
118
|
+
class PayloadBase(BaseXmlModel):
|
|
119
119
|
"""Represents the base fields for an MMS request payload."""
|
|
120
120
|
|
|
121
|
-
# The XML schema to use for validation
|
|
122
|
-
location: SchemaType = attr(name="noNamespaceSchemaLocation", ns="xsi")
|
|
123
|
-
|
|
124
121
|
|
|
125
122
|
class BaseResponse(BaseXmlModel, Generic[E], tag="BaseResponse"):
|
|
126
123
|
"""Contains the base data extracted from the MMS response in a format we can use."""
|
mms_client/types/transport.py
CHANGED
|
@@ -55,7 +55,7 @@ class Attachment(BaseModel):
|
|
|
55
55
|
name: str = Field(alias="name")
|
|
56
56
|
|
|
57
57
|
# The attachment file data
|
|
58
|
-
data:
|
|
58
|
+
data: str = Field(alias="binaryData")
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
class MmsRequest(BaseModel):
|
|
@@ -83,7 +83,7 @@ class MmsRequest(BaseModel):
|
|
|
83
83
|
signature: str = Field(alias="requestSignature")
|
|
84
84
|
|
|
85
85
|
# The base-64 encoded payload of the request
|
|
86
|
-
payload:
|
|
86
|
+
payload: str = Field(alias="requestData")
|
|
87
87
|
|
|
88
88
|
# Any attached files to be sent with the request. Only 20 of these are allowed for OMI requests. For MI requests,
|
|
89
89
|
# the limit is 40.
|
|
@@ -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
|
|
@@ -80,7 +80,7 @@ class Serializer:
|
|
|
80
80
|
|
|
81
81
|
# Finally, convert the payload to XML and return it
|
|
82
82
|
# NOTE: we provided the encoding here so this will return bytes, not a string
|
|
83
|
-
return
|
|
83
|
+
return self._to_canoncialized_xml(payload)
|
|
84
84
|
|
|
85
85
|
def serialize_multi(self, request_envelope: E, request_data: List[P], request_type: Type[P]) -> bytes:
|
|
86
86
|
"""Serialize the envelope and data to a byte string for sending to the MMS server.
|
|
@@ -104,7 +104,7 @@ class Serializer:
|
|
|
104
104
|
|
|
105
105
|
# Finally, convert the payload to XML and return it
|
|
106
106
|
# NOTE: we provided the encoding here so this will return bytes, not a string
|
|
107
|
-
return
|
|
107
|
+
return self._to_canoncialized_xml(payload)
|
|
108
108
|
|
|
109
109
|
def deserialize(self, data: bytes, envelope_type: Type[E], data_type: Type[P]) -> Response[E, P]:
|
|
110
110
|
"""Deserialize the data to a response object.
|
|
@@ -132,6 +132,30 @@ class Serializer:
|
|
|
132
132
|
tree = self._from_xml(data)
|
|
133
133
|
return self._from_tree_multi(tree, envelope_type, data_type)
|
|
134
134
|
|
|
135
|
+
def _to_canoncialized_xml(self, payload: PayloadBase) -> bytes:
|
|
136
|
+
"""Convert the payload to a canonicalized XML string.
|
|
137
|
+
|
|
138
|
+
Arguments:
|
|
139
|
+
payload (PayloadBase): The payload to be converted.
|
|
140
|
+
|
|
141
|
+
Returns: The canonicalized XML string.
|
|
142
|
+
"""
|
|
143
|
+
# First, convert the payload to a raw XML string
|
|
144
|
+
raw: bytes = payload.to_xml(
|
|
145
|
+
skip_empty=True,
|
|
146
|
+
encoding="utf-8",
|
|
147
|
+
xml_declaration=False,
|
|
148
|
+
) # type: ignore[assignment]
|
|
149
|
+
|
|
150
|
+
# Next, parse it back into an XML tree
|
|
151
|
+
unparsed = parse(BytesIO(raw))
|
|
152
|
+
|
|
153
|
+
# Finally, convert the XML tree to a canonicalized XML string and return it
|
|
154
|
+
buffer = BytesIO()
|
|
155
|
+
unparsed.write_c14n(buffer)
|
|
156
|
+
buffer.seek(0)
|
|
157
|
+
return buffer.read()
|
|
158
|
+
|
|
135
159
|
def _from_tree(self, raw: Element, envelope_type: Type[E], data_type: Type[P]) -> Response[E, P]:
|
|
136
160
|
"""Convert the raw data to a response object.
|
|
137
161
|
|
mms_client/utils/web.py
CHANGED
|
@@ -12,13 +12,13 @@ from requests import Session
|
|
|
12
12
|
from requests_pkcs12 import Pkcs12Adapter
|
|
13
13
|
from zeep import Client
|
|
14
14
|
from zeep import Plugin
|
|
15
|
-
from zeep import Transport
|
|
16
15
|
from zeep.cache import SqliteCache
|
|
17
16
|
from zeep.exceptions import TransportError
|
|
18
17
|
from zeep.xsd.valueobjects import CompoundValue
|
|
19
18
|
|
|
20
19
|
from mms_client.types.transport import MmsRequest
|
|
21
20
|
from mms_client.types.transport import MmsResponse
|
|
21
|
+
from mms_client.utils.multipart_transport import MultipartTransport
|
|
22
22
|
|
|
23
23
|
# Set the default logger for the MMS client
|
|
24
24
|
logger = getLogger(__name__)
|
|
@@ -134,6 +134,7 @@ class ZWrapper:
|
|
|
134
134
|
|
|
135
135
|
def __init__(
|
|
136
136
|
self,
|
|
137
|
+
domain: str,
|
|
137
138
|
client: ClientType,
|
|
138
139
|
interface: Interface,
|
|
139
140
|
adapter: Pkcs12Adapter,
|
|
@@ -144,6 +145,7 @@ class ZWrapper:
|
|
|
144
145
|
"""Create a new Zeep wrapper object for a specific MMS service endpoint.
|
|
145
146
|
|
|
146
147
|
Arguments:
|
|
148
|
+
domain (str): The domain to use when signing the content ID to MTOM attachments.
|
|
147
149
|
client (ClientType): The type of client to use. This can be either "bsp" (Balancing Service Provider) or
|
|
148
150
|
"tso" (Transmission System Operator). This will determine which service endpoint to
|
|
149
151
|
use.
|
|
@@ -191,13 +193,25 @@ class ZWrapper:
|
|
|
191
193
|
|
|
192
194
|
# Finally, we create the Zeep client with the given WSDL file location, session, and cache settings and then,
|
|
193
195
|
# from that client, we create the SOAP service with the given service binding and selected endpoint.
|
|
196
|
+
self._transport = MultipartTransport(domain, cache=SqliteCache() if cache else None, session=sess)
|
|
194
197
|
self._client = Client(
|
|
195
198
|
wsdl=str(location.resolve()),
|
|
196
|
-
transport=
|
|
199
|
+
transport=self._transport,
|
|
197
200
|
plugins=plugins,
|
|
198
201
|
)
|
|
199
202
|
self._create_service()
|
|
200
203
|
|
|
204
|
+
def register_attachment(self, name: str, attachment: bytes) -> str:
|
|
205
|
+
"""Register a multipart attachment.
|
|
206
|
+
|
|
207
|
+
Arguments:
|
|
208
|
+
name (str): The name of the attachment.
|
|
209
|
+
attachment (bytes): The data to be attached.
|
|
210
|
+
|
|
211
|
+
Returns: The content ID of the attachment, which should be used in place of the attachment data.
|
|
212
|
+
"""
|
|
213
|
+
return self._transport.register_attachment(name, attachment)
|
|
214
|
+
|
|
201
215
|
@on_exception(expo, TransportError, max_tries=3, giveup=fatal_code, logger=logger) # type: ignore[arg-type]
|
|
202
216
|
def submit(self, req: MmsRequest) -> MmsResponse:
|
|
203
217
|
"""Submit the given request to the MMS server and return the response.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mms-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: API client for accessing the MMS
|
|
5
5
|
Home-page: https://github.com/ElectroRoute-Japan/mms-client
|
|
6
6
|
Author: Ryan Wood
|
|
@@ -22,6 +22,7 @@ Requires-Dist: backoff (>=2.2.1,<3.0.0)
|
|
|
22
22
|
Requires-Dist: cryptography (>=42.0.5,<43.0.0)
|
|
23
23
|
Requires-Dist: lxml (>=5.1.0,<6.0.0)
|
|
24
24
|
Requires-Dist: pendulum (>=3.0.0,<4.0.0)
|
|
25
|
+
Requires-Dist: pycryptodomex (>=3.20.0,<4.0.0)
|
|
25
26
|
Requires-Dist: pydantic (>=2.6.3,<3.0.0)
|
|
26
27
|
Requires-Dist: pydantic-xml (>=2.9.0,<3.0.0)
|
|
27
28
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
@@ -43,6 +44,9 @@ The underlying API sends and receives XML documents. Each of these request or re
|
|
|
43
44
|
|
|
44
45
|
After the data has been converted and added to the outer request object, it is sent to the appropriate server endpoint via a Zeep client. The client certificate is also injected into the request using a PCKS12 adaptor.
|
|
45
46
|
|
|
47
|
+
## Domain
|
|
48
|
+
The domain to use when signing the content ID to MTOM attachments is specified when creating the client. This is not verified by the server, but it is used to generate the content ID for the MTOM attachments. As such, it is important to ensure that the domain is correct.
|
|
49
|
+
|
|
46
50
|
# Serialization
|
|
47
51
|
This library relies on Pydantic 2 and the pydantic-xml library for serialization/deserialization. As such, any type in this library can be converted to not only XML, but to JSON as well. This is extremely useful if you're trying to build a pass-through API service or something similar.
|
|
48
52
|
|
|
@@ -142,7 +146,7 @@ from mms_client.utils.web import ClientType
|
|
|
142
146
|
cert = Certificate("/path/to/my/cert.p12", "fake_passphrase")
|
|
143
147
|
|
|
144
148
|
# Create a new MMS client
|
|
145
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert)
|
|
149
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert)
|
|
146
150
|
|
|
147
151
|
# Create our request offer
|
|
148
152
|
request_offer = OfferData(
|
|
@@ -176,14 +180,14 @@ There's a lot of code here but it's not terribly difficult to understand. All th
|
|
|
176
180
|
If you want to test your MMS connection, you can try using the test server:
|
|
177
181
|
|
|
178
182
|
```python
|
|
179
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
183
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
180
184
|
```
|
|
181
185
|
|
|
182
186
|
## Connecting as a Market Admin
|
|
183
187
|
If you're connecting as a market operator (MO), you can connect in admin mode:
|
|
184
188
|
|
|
185
189
|
```python
|
|
186
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
190
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
187
191
|
```
|
|
188
192
|
|
|
189
193
|
## Auditing XML Requests & Responses
|
|
@@ -202,7 +206,7 @@ class TestAuditPlugin(AuditPlugin):
|
|
|
202
206
|
def audit_response(self, mms_response: bytes) -> None:
|
|
203
207
|
self.response = mms_response
|
|
204
208
|
|
|
205
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, plugins=[TestAuditPlugin()])
|
|
209
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, plugins=[TestAuditPlugin()])
|
|
206
210
|
```
|
|
207
211
|
|
|
208
212
|
This same input allows for the user to create their own plugins and add them to the Zeep client, allowing for a certain amount of extensibility.
|
|
@@ -9,17 +9,17 @@ mms_client/schemas/xsd/mi-report.xsd,sha256=XEHhHCGgK4aeYsmObIrlzkvV8UhbirgoysAE
|
|
|
9
9
|
mms_client/schemas/xsd/mpr.xsd,sha256=QcnuKFm1WkyZfT_56cINHldZkR-3pU4nHSbFKHkIQS0,69149
|
|
10
10
|
mms_client/schemas/xsd/omi.xsd,sha256=benkYeno_HF6BOIv7gAT79dWz00JccS3XilPLBCKVIU,31524
|
|
11
11
|
mms_client/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
mms_client/security/certs.py,sha256=
|
|
13
|
-
mms_client/security/crypto.py,sha256=
|
|
12
|
+
mms_client/security/certs.py,sha256=Gy-CuSsdLPFeoPH_sEYhY67dI5sy6yJ8iTwlysRKT1s,3018
|
|
13
|
+
mms_client/security/crypto.py,sha256=u9Z6nkAW6LbBqUzjIEbZ-CcqdkMJ9fqvdX7IXTTh1EI,2345
|
|
14
14
|
mms_client/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
mms_client/services/base.py,sha256=
|
|
16
|
-
mms_client/services/market.py,sha256=
|
|
15
|
+
mms_client/services/base.py,sha256=T6D9vDsNmz68rLAVbzCl9WQAHqYQ7D18JVFi-L_LkSs,27914
|
|
16
|
+
mms_client/services/market.py,sha256=hQnMZ52abjcQO3L0d5bPuJz7hmBEAbDq2l12I9YUG7k,9153
|
|
17
17
|
mms_client/services/omi.py,sha256=UG1zYkFz0sFsEbhE6P0CLoAOZZOyEshkZ_b7D_e3CjQ,626
|
|
18
18
|
mms_client/services/registration.py,sha256=ryj2WKJoBzRU1r6Svbl2MyGOG0H5aEYAK6HQWL9kYaA,3815
|
|
19
19
|
mms_client/services/report.py,sha256=ZXYDaknPCnSYZI857QHbkzst1Pez_PrKFudVF9bQB7Q,642
|
|
20
20
|
mms_client/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
mms_client/types/award.py,sha256=BWE9V_KHXpg_cW1LZsetVrPs2hZDOklvRpNnoZtmR3k,14139
|
|
22
|
-
mms_client/types/base.py,sha256=
|
|
22
|
+
mms_client/types/base.py,sha256=YMRGCBBDMGRlR7YpDBSDVDvhjEJrxA32YhmiHoXXz-Q,9530
|
|
23
23
|
mms_client/types/enums.py,sha256=YJ58FbhyQ0TVlf8Z-Dg1UfVu8CurY5b21Cy5-WYkJ0I,1629
|
|
24
24
|
mms_client/types/fields.py,sha256=pa5qvQVwEr8dh44IGHyYqgJYTYyTIeAjBW6CylXrkP0,14785
|
|
25
25
|
mms_client/types/market.py,sha256=IbXsH4Q5MJI-CEvGvZlzv2S36mX_Ea02U11Ik-NwSxQ,2706
|
|
@@ -27,13 +27,14 @@ mms_client/types/offer.py,sha256=KosFiKRMnt7XwlLBUfjHUGHiWzrMJUPPhGQMxgdeepM,679
|
|
|
27
27
|
mms_client/types/registration.py,sha256=Nir73S3ffpk0O_fnTD2alFaqV1k67_8dcyyduXvPBI4,1381
|
|
28
28
|
mms_client/types/reserve.py,sha256=pLV47w_749EIVhj0tUuJdWdHBBEl0-v10oVioccgxnU,2667
|
|
29
29
|
mms_client/types/resource.py,sha256=TQnY3JLHRgQhQrG6ISquw-BQgKSr8TGuqn9ItWxWz_w,65974
|
|
30
|
-
mms_client/types/transport.py,sha256=
|
|
30
|
+
mms_client/types/transport.py,sha256=PZ7mDKeH8rGOVONk0ZH5herft71PFF-MUpp3uB57WXo,4395
|
|
31
31
|
mms_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
32
32
|
mms_client/utils/auditing.py,sha256=JDcvNo4ul66xPtDeeocn568yIe8fhh-jM11MWP-Kfes,2057
|
|
33
33
|
mms_client/utils/errors.py,sha256=6k-NOjGZyTbTUISzN7B4JrmU2P8cwjpFFmFC7kJOQFQ,3005
|
|
34
|
-
mms_client/utils/
|
|
35
|
-
mms_client/utils/
|
|
36
|
-
mms_client
|
|
37
|
-
mms_client-1.
|
|
38
|
-
mms_client-1.
|
|
39
|
-
mms_client-1.
|
|
34
|
+
mms_client/utils/multipart_transport.py,sha256=O374vPh2j29_CSjkWnIaPJfiabRSGNpQvNQcUODdkDM,8871
|
|
35
|
+
mms_client/utils/serialization.py,sha256=kGPOWqOxHJ4IPyVB2xRiG6cKMqhUBSNBWJ4bF6xbxH8,30293
|
|
36
|
+
mms_client/utils/web.py,sha256=7c6Ghs3Y52cm2ge-9svR39uQjr2Pm2LhX9Wz-S1APa4,10816
|
|
37
|
+
mms_client-1.7.0.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
38
|
+
mms_client-1.7.0.dist-info/METADATA,sha256=HWPxyyd84Fr9HnM7058QKp_yQRsnw0l5sefy8gb2yto,16447
|
|
39
|
+
mms_client-1.7.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
40
|
+
mms_client-1.7.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|