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.
@@ -17,7 +17,7 @@ class RequestType(Enum):
17
17
 
18
18
  INFO = "mp.info"
19
19
  MARKET = "mp.market"
20
- REGISTRATION = "mp.registration"
20
+ REGISTRATION = "mpr"
21
21
  REPORT = "mp.report"
22
22
  OMI = "mp.omi"
23
23
 
@@ -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), # type: ignore[arg-type]
66
- type(request_data), # type: ignore[arg-type]
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.__name__), 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.__name__) # type: ignore[misc]
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 name, field in current_type.model_fields.items():
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.__name__): # type: ignore[call-arg]
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(key: str, envelope_type: Type[E], data_type: Type[P]) -> Type[PayloadBase]:
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 a wrapper for our data type that will be used to store the data in the payload
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: data_type = element(tag=data_type.__name__) # type: ignore[valid-type]
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: data_type): # type: ignore[valid-type]
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
- # Next, create our payload type that actually contains all the XML data
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: data_type, schema: str): # type: ignore[valid-type]
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.6
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
- ![MMS Client Object Hierarchy](https://github.com/ElectroRoute-Japan/mms-client/blob/main/docs/mmsclient_hierarchy.drawio.png)
60
+ ![MMS Client Object Hierarchy](https://github.com/ElectroRoute-Japan/mms-client/raw/main/docs/mmsclient_hierarchy.drawio.png)
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=8yWBeDoBimY9BZeMOkMDV-ggvPTzZHnlqXiv9eo8fIQ,25363
16
- mms_client/services/market.py,sha256=kwcbXz1Op4hwXbOn_Xsx_qBNuHfvWAxBH6G2f4AWzPI,4892
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=MUUp0SDE7F0d_lV5pXNqPhrU87CAZc6AA72AdmRAu6Q,571
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=EMGCDvf_B_8q1YMH-l31c_lV0OdxxAReFNU3zPs0K0Q,9681
22
- mms_client/types/enums.py,sha256=WzFD9fG9jKFsA91oKyXCAW0oKzUTH3NyUkV4A_N0sXg,329
23
- mms_client/types/fields.py,sha256=QaNnfU0goAld8VADD_DXZ6qEuUrxISY7rFg5gB6k948,6250
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=EdxgHlU9FWPuMq3tLYcbILBWnL5SWMsH5Qe7NRhGz9Y,6300
26
- mms_client/types/transport.py,sha256=SGzDGoTpUvhGpkKxoyJR22z-Ao_Dj5idz97vd6u7fqk,4419
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=Zp1pZvrWcbV4p_PERS5e9zNKPSeHQFijYzJiby3y0Pk,25206
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.6.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
32
- mms_client-1.0.6.dist-info/METADATA,sha256=sImN-laCo5emcrt6KlfJZGmdb85XlBbTumjRlmIkUZQ,13840
33
- mms_client-1.0.6.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
34
- mms_client-1.0.6.dist-info/RECORD,,
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,,