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.
Files changed (44) hide show
  1. {mms_client-1.6.0 → mms_client-1.8.0}/PKG-INFO +14 -5
  2. {mms_client-1.6.0 → mms_client-1.8.0}/README.md +11 -4
  3. {mms_client-1.6.0 → mms_client-1.8.0}/pyproject.toml +3 -2
  4. mms_client-1.8.0/src/mms_client/security/certs.py +77 -0
  5. mms_client-1.8.0/src/mms_client/security/crypto.py +65 -0
  6. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/base.py +133 -66
  7. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/market.py +31 -12
  8. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/registration.py +12 -9
  9. mms_client-1.8.0/src/mms_client/services/report.py +161 -0
  10. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/base.py +53 -13
  11. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/enums.py +34 -0
  12. mms_client-1.8.0/src/mms_client/types/report.py +474 -0
  13. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/resource.py +4 -34
  14. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/transport.py +2 -2
  15. mms_client-1.8.0/src/mms_client/utils/multipart_transport.py +259 -0
  16. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/serialization.py +137 -44
  17. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/web.py +16 -2
  18. mms_client-1.6.0/src/mms_client/security/certs.py +0 -44
  19. mms_client-1.6.0/src/mms_client/security/crypto.py +0 -57
  20. mms_client-1.6.0/src/mms_client/services/report.py +0 -18
  21. {mms_client-1.6.0 → mms_client-1.8.0}/LICENSE +0 -0
  22. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/__init__.py +0 -0
  23. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/client.py +0 -0
  24. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/py.typed +0 -0
  25. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/wsdl/mi-web-service-jbms.wsdl +0 -0
  26. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/wsdl/omi-web-service.wsdl +0 -0
  27. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mi-market.xsd +0 -0
  28. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mi-outbnd-reports.xsd +0 -0
  29. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mi-report.xsd +0 -0
  30. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/mpr.xsd +0 -0
  31. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/schemas/xsd/omi.xsd +0 -0
  32. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/security/__init__.py +0 -0
  33. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/__init__.py +0 -0
  34. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/services/omi.py +0 -0
  35. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/__init__.py +0 -0
  36. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/award.py +0 -0
  37. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/fields.py +0 -0
  38. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/market.py +0 -0
  39. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/offer.py +0 -0
  40. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/registration.py +0 -0
  41. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/types/reserve.py +0 -0
  42. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/__init__.py +0 -0
  43. {mms_client-1.6.0 → mms_client-1.8.0}/src/mms_client/utils/auditing.py +0 -0
  44. {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.6.0
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.6.0"
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)