mms-client 1.6.0__tar.gz → 1.8.0__tar.gz
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-1.6.0 → mms_client-1.8.0}/PKG-INFO +14 -5
- {mms_client-1.6.0 → mms_client-1.8.0}/README.md +11 -4
- {mms_client-1.6.0 → mms_client-1.8.0}/pyproject.toml +3 -2
- mms_client-1.8.0/src/mms_client/security/certs.py +77 -0
- mms_client-1.8.0/src/mms_client/security/crypto.py +65 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/base.py +133 -66
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/market.py +31 -12
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/registration.py +12 -9
- mms_client-1.8.0/src/mms_client/services/report.py +161 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/base.py +53 -13
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/enums.py +34 -0
- mms_client-1.8.0/src/mms_client/types/report.py +474 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/resource.py +4 -34
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/transport.py +2 -2
- mms_client-1.8.0/src/mms_client/utils/multipart_transport.py +259 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/serialization.py +137 -44
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/web.py +16 -2
- mms_client-1.6.0/src/mms_client/security/certs.py +0 -44
- mms_client-1.6.0/src/mms_client/security/crypto.py +0 -57
- mms_client-1.6.0/src/mms_client/services/report.py +0 -18
- {mms_client-1.6.0 → mms_client-1.8.0}/LICENSE +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/__init__.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/client.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/py.typed +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/wsdl/mi-web-service-jbms.wsdl +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/wsdl/omi-web-service.wsdl +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mi-market.xsd +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mi-outbnd-reports.xsd +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mi-report.xsd +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mpr.xsd +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/omi.xsd +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/security/__init__.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/__init__.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/omi.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/__init__.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/award.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/fields.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/market.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/offer.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/registration.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/reserve.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/__init__.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/auditing.py +0 -0
- {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/errors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mms-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.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,7 +22,9 @@ 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)
|
|
27
|
+
Requires-Dist: pydantic-extra-types (>=2.7.0,<3.0.0)
|
|
26
28
|
Requires-Dist: pydantic-xml (>=2.9.0,<3.0.0)
|
|
27
29
|
Requires-Dist: requests (>=2.31.0,<3.0.0)
|
|
28
30
|
Requires-Dist: requests-pkcs12 (>=1.24,<2.0)
|
|
@@ -43,6 +45,9 @@ The underlying API sends and receives XML documents. Each of these request or re
|
|
|
43
45
|
|
|
44
46
|
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
47
|
|
|
48
|
+
## Domain
|
|
49
|
+
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.
|
|
50
|
+
|
|
46
51
|
# Serialization
|
|
47
52
|
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
53
|
|
|
@@ -142,7 +147,7 @@ from mms_client.utils.web import ClientType
|
|
|
142
147
|
cert = Certificate("/path/to/my/cert.p12", "fake_passphrase")
|
|
143
148
|
|
|
144
149
|
# Create a new MMS client
|
|
145
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert)
|
|
150
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert)
|
|
146
151
|
|
|
147
152
|
# Create our request offer
|
|
148
153
|
request_offer = OfferData(
|
|
@@ -176,14 +181,14 @@ There's a lot of code here but it's not terribly difficult to understand. All th
|
|
|
176
181
|
If you want to test your MMS connection, you can try using the test server:
|
|
177
182
|
|
|
178
183
|
```python
|
|
179
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
184
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
180
185
|
```
|
|
181
186
|
|
|
182
187
|
## Connecting as a Market Admin
|
|
183
188
|
If you're connecting as a market operator (MO), you can connect in admin mode:
|
|
184
189
|
|
|
185
190
|
```python
|
|
186
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
191
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
187
192
|
```
|
|
188
193
|
|
|
189
194
|
## Auditing XML Requests & Responses
|
|
@@ -202,7 +207,7 @@ class TestAuditPlugin(AuditPlugin):
|
|
|
202
207
|
def audit_response(self, mms_response: bytes) -> None:
|
|
203
208
|
self.response = mms_response
|
|
204
209
|
|
|
205
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, plugins=[TestAuditPlugin()])
|
|
210
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, plugins=[TestAuditPlugin()])
|
|
206
211
|
```
|
|
207
212
|
|
|
208
213
|
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.
|
|
@@ -216,6 +221,10 @@ This client is not complete. Currently, it supports the following endpoints:
|
|
|
216
221
|
- MarketQuery_AwardResultsQuery
|
|
217
222
|
- RegistrationSubmit_Resource
|
|
218
223
|
- RegistrationQuery_Resource
|
|
224
|
+
- ReportCreateRequest
|
|
225
|
+
- ReportListRequest
|
|
226
|
+
- ReportDownloadRequestTrnID
|
|
227
|
+
- BSP_ResourceList
|
|
219
228
|
|
|
220
229
|
We can add support for additional endpoints as time goes on, and independent contribution is, of course, welcome. However, support for attachments is currently limited because none of the endpoints we support currently require them. We have implemented attachment support up to the client level, but we haven't developed an architecture for submitting them through an endpoint yet.
|
|
221
230
|
|
|
@@ -11,6 +11,9 @@ The underlying API sends and receives XML documents. Each of these request or re
|
|
|
11
11
|
|
|
12
12
|
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.
|
|
13
13
|
|
|
14
|
+
## Domain
|
|
15
|
+
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.
|
|
16
|
+
|
|
14
17
|
# Serialization
|
|
15
18
|
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.
|
|
16
19
|
|
|
@@ -110,7 +113,7 @@ from mms_client.utils.web import ClientType
|
|
|
110
113
|
cert = Certificate("/path/to/my/cert.p12", "fake_passphrase")
|
|
111
114
|
|
|
112
115
|
# Create a new MMS client
|
|
113
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert)
|
|
116
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert)
|
|
114
117
|
|
|
115
118
|
# Create our request offer
|
|
116
119
|
request_offer = OfferData(
|
|
@@ -144,14 +147,14 @@ There's a lot of code here but it's not terribly difficult to understand. All th
|
|
|
144
147
|
If you want to test your MMS connection, you can try using the test server:
|
|
145
148
|
|
|
146
149
|
```python
|
|
147
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
150
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
148
151
|
```
|
|
149
152
|
|
|
150
153
|
## Connecting as a Market Admin
|
|
151
154
|
If you're connecting as a market operator (MO), you can connect in admin mode:
|
|
152
155
|
|
|
153
156
|
```python
|
|
154
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
157
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
155
158
|
```
|
|
156
159
|
|
|
157
160
|
## Auditing XML Requests & Responses
|
|
@@ -170,7 +173,7 @@ class TestAuditPlugin(AuditPlugin):
|
|
|
170
173
|
def audit_response(self, mms_response: bytes) -> None:
|
|
171
174
|
self.response = mms_response
|
|
172
175
|
|
|
173
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, plugins=[TestAuditPlugin()])
|
|
176
|
+
client = MmsClient(domain="mydomain.com", participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, plugins=[TestAuditPlugin()])
|
|
174
177
|
```
|
|
175
178
|
|
|
176
179
|
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.
|
|
@@ -184,6 +187,10 @@ This client is not complete. Currently, it supports the following endpoints:
|
|
|
184
187
|
- MarketQuery_AwardResultsQuery
|
|
185
188
|
- RegistrationSubmit_Resource
|
|
186
189
|
- RegistrationQuery_Resource
|
|
190
|
+
- ReportCreateRequest
|
|
191
|
+
- ReportListRequest
|
|
192
|
+
- ReportDownloadRequestTrnID
|
|
193
|
+
- BSP_ResourceList
|
|
187
194
|
|
|
188
195
|
We can add support for additional endpoints as time goes on, and independent contribution is, of course, welcome. However, support for attachments is currently limited because none of the endpoints we support currently require them. We have implemented attachment support up to the client level, but we haven't developed an architecture for submitting them through an endpoint yet.
|
|
189
196
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "mms_client"
|
|
3
|
-
version = "v1.
|
|
3
|
+
version = "v1.8.0"
|
|
4
4
|
description = "API client for accessing the MMS"
|
|
5
5
|
authors = ["Ryan Wood <ryan.wood@electroroute.co.jp>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -32,6 +32,8 @@ cryptography = "^42.0.5"
|
|
|
32
32
|
pydantic-xml = "^2.9.0"
|
|
33
33
|
lxml = "^5.1.0"
|
|
34
34
|
backoff = "^2.2.1"
|
|
35
|
+
pycryptodomex = "^3.20.0"
|
|
36
|
+
pydantic-extra-types = "^2.7.0"
|
|
35
37
|
|
|
36
38
|
[tool.poetry.group.dev.dependencies]
|
|
37
39
|
black = "^24.2.0"
|
|
@@ -48,7 +50,6 @@ pytest-mock = "^3.12.0"
|
|
|
48
50
|
mock = "^5.1.0"
|
|
49
51
|
lxml-stubs = "^0.5.1"
|
|
50
52
|
pyfakefs = "^5.3.5"
|
|
51
|
-
pydantic-extra-types = "^2.6.0"
|
|
52
53
|
responses = "^0.25.0"
|
|
53
54
|
types-urllib3 = "^1.26.25.14"
|
|
54
55
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Contains functionality associated with certificates."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from ssl import PROTOCOL_TLSv1_2
|
|
5
|
+
from typing import Union
|
|
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
|
|
13
|
+
from requests_pkcs12 import Pkcs12Adapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Certificate:
|
|
17
|
+
"""Describes a certificate composed of a cert file and a key file."""
|
|
18
|
+
|
|
19
|
+
# The encoding to use for MMS certificates
|
|
20
|
+
encoding = Encoding.PEM
|
|
21
|
+
|
|
22
|
+
def __init__(self, cert: Union[str, Path, bytes], passphrase: str):
|
|
23
|
+
"""Create a new Certificate.
|
|
24
|
+
|
|
25
|
+
Creates a new certificate from the absolute path to the certificate file and a passpharse.
|
|
26
|
+
|
|
27
|
+
Arguments:
|
|
28
|
+
cert: The certificate file. If a string, it should be the absolute path to the certificate file. If
|
|
29
|
+
bytes, it should be the contents of the certificate file.
|
|
30
|
+
passphrase: The passphrase the certificate is encrypted with
|
|
31
|
+
"""
|
|
32
|
+
# Get the certificate file contents
|
|
33
|
+
if isinstance(cert, (str, Path)):
|
|
34
|
+
with open(cert, "rb") as file:
|
|
35
|
+
self._cert = file.read()
|
|
36
|
+
else:
|
|
37
|
+
self._cert = cert
|
|
38
|
+
|
|
39
|
+
# Save the passphrase
|
|
40
|
+
self._passphrase = passphrase
|
|
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
|
+
|
|
49
|
+
@property
|
|
50
|
+
def certificate(self) -> bytes:
|
|
51
|
+
"""Return the certificate data."""
|
|
52
|
+
return self._cert
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def passphrase(self) -> str:
|
|
56
|
+
"""Return the full path to the passphrase."""
|
|
57
|
+
return self._passphrase
|
|
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
|
+
|
|
75
|
+
def to_adapter(self) -> Pkcs12Adapter:
|
|
76
|
+
"""Convert the certificate to a Pkcs12Adapter."""
|
|
77
|
+
return Pkcs12Adapter(pkcs12_data=self._cert, pkcs12_password=self._passphrase, ssl_protocol=PROTOCOL_TLSv1_2)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Contains objects for cryptographic operations."""
|
|
2
|
+
|
|
3
|
+
from base64 import b64decode
|
|
4
|
+
from base64 import b64encode
|
|
5
|
+
|
|
6
|
+
from Cryptodome.Hash import SHA256
|
|
7
|
+
from Cryptodome.PublicKey import RSA
|
|
8
|
+
from Cryptodome.Signature import pkcs1_15
|
|
9
|
+
|
|
10
|
+
from mms_client.security.certs import Certificate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CryptoWrapper:
|
|
14
|
+
"""Wraps the cryptographic operations necessary for signing and encrypting MMS payload data."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, cert: Certificate):
|
|
17
|
+
"""Create a new CryptoWrapper with the given certificate.
|
|
18
|
+
|
|
19
|
+
Arguments:
|
|
20
|
+
cert (Certificate): The certificate to use for cryptographic operations.
|
|
21
|
+
"""
|
|
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())
|
|
25
|
+
|
|
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)
|
|
29
|
+
|
|
30
|
+
def verify(self, content: bytes, signature: bytes) -> bool:
|
|
31
|
+
"""Verify a signature against the given content using the certificate.
|
|
32
|
+
|
|
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
|
|
49
|
+
|
|
50
|
+
def sign(self, data: bytes) -> bytes:
|
|
51
|
+
"""Create a signature from the given data using the certificate.
|
|
52
|
+
|
|
53
|
+
Arguments:
|
|
54
|
+
data (bytes): The data to be encrypted.
|
|
55
|
+
|
|
56
|
+
Returns: A base64-encoded string containing the signature.
|
|
57
|
+
"""
|
|
58
|
+
# First, hash the data using SHA256
|
|
59
|
+
hashed = SHA256.new(data)
|
|
60
|
+
|
|
61
|
+
# Next, sign the hash using the private key
|
|
62
|
+
signature = self._signer.sign(hashed)
|
|
63
|
+
|
|
64
|
+
# Finally, return the base64-encoded signature
|
|
65
|
+
return b64encode(signature)
|