mms-client 1.0.6__py3-none-any.whl → 1.2.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/services/base.py +18 -6
- mms_client/services/market.py +26 -0
- mms_client/services/registration.py +64 -0
- mms_client/types/base.py +3 -1
- mms_client/types/enums.py +8 -0
- mms_client/types/fields.py +150 -2
- mms_client/types/offer.py +16 -4
- mms_client/types/registration.py +47 -0
- mms_client/types/resource.py +1064 -0
- mms_client/types/transport.py +1 -1
- mms_client/utils/serialization.py +60 -15
- {mms_client-1.0.6.dist-info → mms_client-1.2.0.dist-info}/METADATA +18 -2
- {mms_client-1.0.6.dist-info → mms_client-1.2.0.dist-info}/RECORD +15 -13
- {mms_client-1.0.6.dist-info → mms_client-1.2.0.dist-info}/LICENSE +0 -0
- {mms_client-1.0.6.dist-info → mms_client-1.2.0.dist-info}/WHEEL +0 -0
mms_client/types/transport.py
CHANGED
|
@@ -4,6 +4,7 @@ from functools import lru_cache
|
|
|
4
4
|
from io import BytesIO
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Dict
|
|
7
|
+
from typing import List
|
|
7
8
|
from typing import Optional
|
|
8
9
|
from typing import Tuple
|
|
9
10
|
from typing import Type
|
|
@@ -62,8 +63,33 @@ class Serializer:
|
|
|
62
63
|
# First, create our payload class from the payload and data types
|
|
63
64
|
payload_cls = _create_request_payload_type(
|
|
64
65
|
self._payload_key,
|
|
65
|
-
type(request_envelope),
|
|
66
|
-
type(request_data),
|
|
66
|
+
type(request_envelope),
|
|
67
|
+
type(request_data),
|
|
68
|
+
False, # type: ignore[arg-type]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Next, inject the payload and data into the payload class
|
|
72
|
+
# Note: this returns a type that inherits from PayloadBase and the arguments provided to the initializer
|
|
73
|
+
# here are correct, but mypy thinks they are incorrect because it doesn't understand the the inherited type
|
|
74
|
+
payload = payload_cls(request_envelope, request_data, self._xsd.value) # type: ignore[call-arg, misc]
|
|
75
|
+
|
|
76
|
+
# Finally, convert the payload to XML and return it
|
|
77
|
+
# Note: we provided the encoding here so this will return bytes, not a string
|
|
78
|
+
return payload.to_xml(skip_empty=True, encoding="utf-8", xml_declaration=True) # type: ignore[return-value]
|
|
79
|
+
|
|
80
|
+
def serialize_multi(self, request_envelope: E, request_data: List[P], request_type: Type[P]) -> bytes:
|
|
81
|
+
"""Serialize the envelope and data to a byte string for sending to the MMS server.
|
|
82
|
+
|
|
83
|
+
Arguments:
|
|
84
|
+
request_envelope (Envelope): The envelope to be serialized.
|
|
85
|
+
request_data (List[Payload]): The data to be serialized.
|
|
86
|
+
request_type (Type[Payload]): The type of data to be serialized.
|
|
87
|
+
|
|
88
|
+
Returns: A byte string containing the XML-formatted data to be sent to the MMS server.
|
|
89
|
+
"""
|
|
90
|
+
# First, create our payload class from the payload and data types
|
|
91
|
+
payload_cls = _create_request_payload_type(
|
|
92
|
+
self._payload_key, type(request_envelope), request_type, True # type: ignore[arg-type]
|
|
67
93
|
)
|
|
68
94
|
|
|
69
95
|
# Next, inject the payload and data into the payload class
|
|
@@ -129,7 +155,7 @@ class Serializer:
|
|
|
129
155
|
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
130
156
|
# from within the envelope
|
|
131
157
|
self._verify_tree_data_tag(envelope_node, data_type)
|
|
132
|
-
resp.payload = self._from_tree_data(envelope_node.find(data_type
|
|
158
|
+
resp.payload = self._from_tree_data(envelope_node.find(get_tag(data_type)), data_type)
|
|
133
159
|
|
|
134
160
|
# Finally, attempt to extract the messages from within the payload
|
|
135
161
|
resp.messages = self._from_tree_messages(raw, envelope_type, data_type, self._payload_key, False)
|
|
@@ -167,7 +193,7 @@ class Serializer:
|
|
|
167
193
|
# Note: apparently, mypy doesn't know about setter-getter properties either...
|
|
168
194
|
self._verify_tree_data_tag(env_node, data_type)
|
|
169
195
|
resp.payload = [
|
|
170
|
-
self._from_tree_data(item, data_type) for item in env_node.findall(data_type
|
|
196
|
+
self._from_tree_data(item, data_type) for item in env_node.findall(get_tag(data_type)) # type: ignore[misc]
|
|
171
197
|
]
|
|
172
198
|
|
|
173
199
|
# Finally, attempt to extract the messages from within the payload
|
|
@@ -216,7 +242,7 @@ class Serializer:
|
|
|
216
242
|
ValueError: If the expected data type is not found in the response.
|
|
217
243
|
"""
|
|
218
244
|
data_tags = set(node.tag for node in raw)
|
|
219
|
-
if not data_tags.issubset([data_type.__name__, "Messages"]):
|
|
245
|
+
if not data_tags.issubset([data_type.__name__, data_type.__xml_tag__, "Messages"]):
|
|
220
246
|
raise ValueError(f"Expected data type '{data_type.__name__}' not found in response")
|
|
221
247
|
|
|
222
248
|
def _from_tree_data(self, raw: Optional[Element], data_type: Type[P]) -> Optional[ResponseData[P]]:
|
|
@@ -291,8 +317,7 @@ class Serializer:
|
|
|
291
317
|
)
|
|
292
318
|
else:
|
|
293
319
|
# Iterate over each field on the current type...
|
|
294
|
-
for
|
|
295
|
-
print(f"Checking field {name} with type {field.annotation}...")
|
|
320
|
+
for field in current_type.model_fields.values():
|
|
296
321
|
|
|
297
322
|
# First, get the arguments and origin of the field's annotation
|
|
298
323
|
args = get_args(field.annotation)
|
|
@@ -420,7 +445,7 @@ def _create_response_payload_type(key: str, envelope_type: Type[E], data_type: T
|
|
|
420
445
|
|
|
421
446
|
|
|
422
447
|
@lru_cache(maxsize=None)
|
|
423
|
-
def _create_response_common_type(tag_type: Type) -> Type[ResponseCommon]:
|
|
448
|
+
def _create_response_common_type(tag_type: Type[Union[E, P]]) -> Type[ResponseCommon]:
|
|
424
449
|
"""Create a new wrapper for the ResponseCommon type with the given tag.
|
|
425
450
|
|
|
426
451
|
This method is intended to save us the overhead of writing a new class for each tag type. Instead, we can
|
|
@@ -432,7 +457,7 @@ def _create_response_common_type(tag_type: Type) -> Type[ResponseCommon]:
|
|
|
432
457
|
Returns: The wrapper type that will be used for deserialization.
|
|
433
458
|
""" # fmt: skip
|
|
434
459
|
# First, create a new wrapper type that contains the ResponseCommon type with the appropriate XML tag
|
|
435
|
-
class Wrapper(ResponseCommon, tag=tag_type
|
|
460
|
+
class Wrapper(ResponseCommon, tag=get_tag(tag_type)): # type: ignore[call-arg]
|
|
436
461
|
"""Wrapper for the validation object with the proper XML tag."""
|
|
437
462
|
|
|
438
463
|
# Finally, return the wrapper type so we can instantiate it
|
|
@@ -440,7 +465,12 @@ def _create_response_common_type(tag_type: Type) -> Type[ResponseCommon]:
|
|
|
440
465
|
|
|
441
466
|
|
|
442
467
|
@lru_cache(maxsize=None)
|
|
443
|
-
def _create_request_payload_type(
|
|
468
|
+
def _create_request_payload_type(
|
|
469
|
+
key: str,
|
|
470
|
+
envelope_type: Type[E],
|
|
471
|
+
data_type: Type[Union[P, List[P]]],
|
|
472
|
+
multi: bool,
|
|
473
|
+
) -> Type[PayloadBase]:
|
|
444
474
|
"""Create a new payload type for the given payload and data types.
|
|
445
475
|
|
|
446
476
|
This method is intended to save us the overhead of writing a new class for each payload type. Instead, we can
|
|
@@ -454,17 +484,21 @@ def _create_request_payload_type(key: str, envelope_type: Type[E], data_type: Ty
|
|
|
454
484
|
key: str The tag to use for the parent element of the payload.
|
|
455
485
|
envelope_type (Type[Envelope]): The type of payload to be constructed.
|
|
456
486
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
487
|
+
multi (bool): If True, the payload will be a list; otherwise, it will be a singleton.
|
|
457
488
|
|
|
458
489
|
Returns: A new payload type that can be used for serialization.
|
|
459
490
|
""" # fmt: skip
|
|
460
|
-
# First, create
|
|
491
|
+
# First, create our data type
|
|
492
|
+
payload_type = List[data_type] if multi else data_type # type: ignore[valid-type]
|
|
493
|
+
|
|
494
|
+
# Next, create a wrapper for our data type that will be used to store the data in the payload
|
|
461
495
|
class Envelope(envelope_type): # type: ignore[valid-type, misc]
|
|
462
496
|
"""Wrapper for the data type that will be used to store the data in the payload."""
|
|
463
497
|
|
|
464
498
|
# The data to be stored in the payload
|
|
465
|
-
data:
|
|
499
|
+
data: payload_type = element(tag=get_tag(data_type)) # type: ignore[valid-type, type-var]
|
|
466
500
|
|
|
467
|
-
def __init__(self, envelope: envelope_type, data:
|
|
501
|
+
def __init__(self, envelope: envelope_type, data: payload_type): # type: ignore[valid-type]
|
|
468
502
|
"""Create a new envelope to store payload data.
|
|
469
503
|
|
|
470
504
|
Arguments:
|
|
@@ -475,14 +509,14 @@ def _create_request_payload_type(key: str, envelope_type: Type[E], data_type: Ty
|
|
|
475
509
|
obj["data"] = data
|
|
476
510
|
super().__init__(**obj)
|
|
477
511
|
|
|
478
|
-
#
|
|
512
|
+
# Now, create our payload type that actually contains all the XML data
|
|
479
513
|
class RQPayload(PayloadBase, tag=key): # type: ignore[call-arg]
|
|
480
514
|
"""The payload type that will be used for serialization."""
|
|
481
515
|
|
|
482
516
|
# The payload containing our request object and any data
|
|
483
517
|
envelope: Envelope = element(tag=envelope_type.__name__)
|
|
484
518
|
|
|
485
|
-
def __init__(self, envelope: envelope_type, data:
|
|
519
|
+
def __init__(self, envelope: envelope_type, data: payload_type, schema: str): # type: ignore[valid-type]
|
|
486
520
|
"""Create a new payload containing the request object and any data.
|
|
487
521
|
|
|
488
522
|
Arguments:
|
|
@@ -511,3 +545,14 @@ def _find_or_fail(node: Element, tag: str) -> Element:
|
|
|
511
545
|
if found is None:
|
|
512
546
|
raise ValueError(f"Expected tag '{tag}' not found in node") # pragma: no cover
|
|
513
547
|
return found
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def get_tag(data_type: Type[P]) -> str:
|
|
551
|
+
"""Get the tag for the given data type.
|
|
552
|
+
|
|
553
|
+
Arguments:
|
|
554
|
+
data_type (Type[Payload]): The data type to get the tag for.
|
|
555
|
+
|
|
556
|
+
Returns: The tag for the given data type.
|
|
557
|
+
"""
|
|
558
|
+
return data_type.__xml_tag__ or data_type.__name__
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mms-client
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: API client for accessing the MMS
|
|
5
|
+
Home-page: https://github.com/ElectroRoute-Japan/mms-client
|
|
5
6
|
Author: Ryan Wood
|
|
6
7
|
Author-email: ryan.wood@electroroute.co.jp
|
|
7
8
|
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Framework :: Pydantic :: 2
|
|
11
|
+
Classifier: Framework :: Pytest
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: The Unlicense (Unlicense)
|
|
14
|
+
Classifier: Natural Language :: English
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
8
16
|
Classifier: Programming Language :: Python :: 3
|
|
9
17
|
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Typing :: Typed
|
|
10
21
|
Requires-Dist: backoff (>=2.2.1,<3.0.0)
|
|
11
22
|
Requires-Dist: cryptography (>=42.0.5,<43.0.0)
|
|
12
23
|
Requires-Dist: lxml (>=5.1.0,<6.0.0)
|
|
@@ -32,6 +43,9 @@ The underlying API sends and receives XML documents. Each of these request or re
|
|
|
32
43
|
|
|
33
44
|
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.
|
|
34
45
|
|
|
46
|
+
# Serialization
|
|
47
|
+
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
|
+
|
|
35
49
|
## Client Types
|
|
36
50
|
Clients cannot call any and all endpoints in this API, willy-nilly. Some endpoints are restricted to particular clients. At the moment, there are two clients: Balance Service Providers (BSPs) and Transmission Service Operators (TSOs). Most likely you're operating as a BSP, in which case you'll have access to all endpoints. However, it makes little sense for a TSO to be able to submit bids on their own power, so they are restricted to a read-only role in most cases.
|
|
37
51
|
|
|
@@ -43,7 +57,7 @@ The MMS has two separate endpoints, depending on whether you want to access mark
|
|
|
43
57
|
# Object Hierarchy
|
|
44
58
|
The object hierarchy contained in this project is not trivial, and perhaps that reflects a bit of overengineering on our part. However, we chose the paradigm we did to reduce the number of types which had to be exposed to the user. The diagram below indicates how this hierarchy works:
|
|
45
59
|
|
|
46
|
-

|
|
47
61
|
|
|
48
62
|
Note that there are some types here that are shared between the request and response: mainly, anything inheriting from `mms_client.types.base.Envelop` or `mms_client.types.base.Payload`. For users of the client, only the Payload types need ever be used. Everything else has been obfuscated away, so it is unecessary for the user to have access to these. However, we will explain our reasoning here to provide additional context.
|
|
49
63
|
|
|
@@ -184,6 +198,8 @@ This client is not complete. Currently, it supports the following endpoints:
|
|
|
184
198
|
- MarketSubmit_OfferData
|
|
185
199
|
- MarketQuery_OfferQuery
|
|
186
200
|
- MarketCancel_OfferCancel
|
|
201
|
+
- RegistrationSubmit_Resource
|
|
202
|
+
- RegistrationQuery_Resource
|
|
187
203
|
|
|
188
204
|
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
205
|
|
|
@@ -12,23 +12,25 @@ mms_client/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
12
12
|
mms_client/security/certs.py,sha256=kNCUFmy18YIxkWKu3mdMmlxmHdft4a6BvtyJ46rA9I4,1489
|
|
13
13
|
mms_client/security/crypto.py,sha256=M7aIllM3_ZwZgm9nH6QQ6Ig14XCAd6e6WGwqqUbbI1Q,2149
|
|
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=mcUL-AZpaJETVkzAOoazmKfI5O3cJ0KWzIYQ5bjpNuE,25839
|
|
16
|
+
mms_client/services/market.py,sha256=juYIZuSsJ10srzDK4N6s8guoGqdDuQXGohecF43WQh8,6169
|
|
17
17
|
mms_client/services/omi.py,sha256=h6cM5U3-iSm0YiIaJwqYTZeI5uhLbA7FPxh_qy_3qww,521
|
|
18
|
-
mms_client/services/registration.py,sha256=
|
|
18
|
+
mms_client/services/registration.py,sha256=9pNZdgwRbJJCnmsVgESNnc3ywkn-wdQryumUhLn6Xvg,3419
|
|
19
19
|
mms_client/services/report.py,sha256=HYVJNwEHo6ZC6497UqO5y1IqZ2ga3kVH5AepdxhYfug,537
|
|
20
20
|
mms_client/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
mms_client/types/base.py,sha256=
|
|
22
|
-
mms_client/types/enums.py,sha256=
|
|
23
|
-
mms_client/types/fields.py,sha256=
|
|
21
|
+
mms_client/types/base.py,sha256=wrDPn9io30in_w2qKa4A503gX_gAaMT7a1MpZ3HcmIc,9701
|
|
22
|
+
mms_client/types/enums.py,sha256=wXlXvifWXWm9dowfG5iQlkax_6OX6z2nHid94WkhQwE,464
|
|
23
|
+
mms_client/types/fields.py,sha256=SAvrDJGSvOVtbGvaaqLFBx2mCr0AxzqVFUTJgPSEDOY,12065
|
|
24
24
|
mms_client/types/market.py,sha256=OKjBIx9aIgaLUci6b5WoB4NZbFncRbbd2FxeAvW1iWw,2588
|
|
25
|
-
mms_client/types/offer.py,sha256=
|
|
26
|
-
mms_client/types/
|
|
25
|
+
mms_client/types/offer.py,sha256=orlohxAWZlW2rwPJpGAoiy1knoCT2x2sXJyzThkXYVY,7693
|
|
26
|
+
mms_client/types/registration.py,sha256=Nir73S3ffpk0O_fnTD2alFaqV1k67_8dcyyduXvPBI4,1381
|
|
27
|
+
mms_client/types/resource.py,sha256=_pRNAqOX8A0lUXmU8qO_8QwRtRx9BwZDdrN-w_Fnu38,66466
|
|
28
|
+
mms_client/types/transport.py,sha256=vyosoeGSdYthqlmiyDAkZusYa8yVHUwOzfTqwZne2Ik,4407
|
|
27
29
|
mms_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
30
|
mms_client/utils/errors.py,sha256=jYdlG4OPI82s0fJcXCRNlKeEixDUSSAxjs_7C16qVL4,2306
|
|
29
|
-
mms_client/utils/serialization.py,sha256=
|
|
31
|
+
mms_client/utils/serialization.py,sha256=TqkucFL1rvMu49sxsWDwCZSvWMDd75Gj9kZmm1FYUzc,27203
|
|
30
32
|
mms_client/utils/web.py,sha256=fcdCtdDrHBPyhIlTcyiuAk3D3TlW8HmUw-wGfpG4KTA,9653
|
|
31
|
-
mms_client-1.0.
|
|
32
|
-
mms_client-1.0.
|
|
33
|
-
mms_client-1.0.
|
|
34
|
-
mms_client-1.0.
|
|
33
|
+
mms_client-1.2.0.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
34
|
+
mms_client-1.2.0.dist-info/METADATA,sha256=xTwJZuKbEW7HKg_C5zw-uVYVXjnI112CiYxEPxGmoWA,14742
|
|
35
|
+
mms_client-1.2.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
36
|
+
mms_client-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|