mms-client 1.9.2__py3-none-any.whl → 1.10.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 +47 -20
- mms_client/services/market.py +1 -1
- mms_client/types/award.py +3 -2
- mms_client/types/base.py +10 -6
- mms_client/types/fields.py +1 -1
- mms_client/types/offer.py +2 -1
- mms_client/types/report.py +2 -2
- mms_client/types/reserve.py +2 -1
- mms_client/types/resource.py +17 -15
- mms_client/types/transport.py +3 -2
- mms_client/utils/errors.py +51 -0
- mms_client/utils/serialization.py +29 -16
- {mms_client-1.9.2.dist-info → mms_client-1.10.0.dist-info}/METADATA +3 -1
- {mms_client-1.9.2.dist-info → mms_client-1.10.0.dist-info}/RECORD +16 -16
- {mms_client-1.9.2.dist-info → mms_client-1.10.0.dist-info}/LICENSE +0 -0
- {mms_client-1.9.2.dist-info → mms_client-1.10.0.dist-info}/WHEEL +0 -0
mms_client/services/base.py
CHANGED
|
@@ -28,6 +28,7 @@ from mms_client.types.transport import RequestDataType
|
|
|
28
28
|
from mms_client.types.transport import RequestType
|
|
29
29
|
from mms_client.types.transport import ResponseDataType
|
|
30
30
|
from mms_client.utils.errors import AudienceError
|
|
31
|
+
from mms_client.utils.errors import EnvelopeNodeNotFoundError
|
|
31
32
|
from mms_client.utils.errors import MMSClientError
|
|
32
33
|
from mms_client.utils.errors import MMSServerError
|
|
33
34
|
from mms_client.utils.errors import MMSValidationError
|
|
@@ -131,7 +132,7 @@ class ClientProto(Protocol):
|
|
|
131
132
|
envelope: E,
|
|
132
133
|
data: Union[P, List[P]],
|
|
133
134
|
config: EndpointConfiguration,
|
|
134
|
-
) -> Tuple[MultiResponse[E, P], Dict[str, bytes]]:
|
|
135
|
+
) -> Tuple[MultiResponse[E, P], Dict[str, bytes], bool]:
|
|
135
136
|
"""Submit a request to the MMS server and return the multi-response.
|
|
136
137
|
|
|
137
138
|
Arguments:
|
|
@@ -139,7 +140,10 @@ class ClientProto(Protocol):
|
|
|
139
140
|
data (Payload): The data to submit to the MMS server.
|
|
140
141
|
config (EndpointConfiguration): The configuration for the endpoint.
|
|
141
142
|
|
|
142
|
-
Returns:
|
|
143
|
+
Returns:
|
|
144
|
+
MultiResponse[E, P]: The multi-response from the MMS server.
|
|
145
|
+
Dict[str, bytes]: The attachments returned with the response.
|
|
146
|
+
bool: Whether or not the response was found.
|
|
143
147
|
"""
|
|
144
148
|
|
|
145
149
|
|
|
@@ -244,15 +248,15 @@ def mms_multi_endpoint(**kwargs):
|
|
|
244
248
|
envelope, callback = result, None
|
|
245
249
|
|
|
246
250
|
# Now, submit the request to the MMS server and get the response
|
|
247
|
-
resp, attachments = self.request_many(envelope, args[0], config)
|
|
251
|
+
resp, attachments, found = self.request_many(envelope, args[0], config)
|
|
248
252
|
|
|
249
253
|
# Call the callback function if it was provided
|
|
250
254
|
if callback:
|
|
251
|
-
callback(resp, attachments) # pragma: no cover
|
|
255
|
+
callback(resp, attachments, found) # pragma: no cover
|
|
252
256
|
|
|
253
257
|
# Finally, extract the data from the response and return it
|
|
254
258
|
logger.info(f"{config.name}: Returning {len(resp.data)} item(s).")
|
|
255
|
-
return resp.data
|
|
259
|
+
return resp.data if found else []
|
|
256
260
|
|
|
257
261
|
return wrapper
|
|
258
262
|
|
|
@@ -382,7 +386,9 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
382
386
|
envelope_type = config.response_envelope_type or type(envelope)
|
|
383
387
|
data_type = config.response_data_type or type(payload)
|
|
384
388
|
deserializer = config.serializer or config.service.serializer
|
|
385
|
-
data: Response[E, P] = deserializer.deserialize(
|
|
389
|
+
data: Response[E, P] = deserializer.deserialize(
|
|
390
|
+
config.name, resp.payload, envelope_type, data_type, config.for_report
|
|
391
|
+
)
|
|
386
392
|
self._verify_response(data, config)
|
|
387
393
|
|
|
388
394
|
# Return the response data and any attachments
|
|
@@ -391,12 +397,12 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
391
397
|
)
|
|
392
398
|
return data, attachments
|
|
393
399
|
|
|
394
|
-
def request_many(
|
|
400
|
+
def request_many( # pylint: disable=too-many-locals
|
|
395
401
|
self,
|
|
396
402
|
envelope: E,
|
|
397
403
|
payload: Union[P, List[P]],
|
|
398
404
|
config: EndpointConfiguration[E, P],
|
|
399
|
-
) -> Tuple[MultiResponse[E, P], Dict[str, bytes]]:
|
|
405
|
+
) -> Tuple[MultiResponse[E, P], Dict[str, bytes], bool]:
|
|
400
406
|
"""Submit a request to the MMS server and return the multi-response.
|
|
401
407
|
|
|
402
408
|
Arguments:
|
|
@@ -404,7 +410,10 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
404
410
|
payload (Payload): The data to submit to the MMS server.
|
|
405
411
|
config (EndpointConfiguration): The configuration for the endpoint.
|
|
406
412
|
|
|
407
|
-
Returns:
|
|
413
|
+
Returns:
|
|
414
|
+
MultiResponse[E, P]: The multi-response from the MMS server.
|
|
415
|
+
Dict[str, bytes]: The attachments returned with the response.
|
|
416
|
+
bool: Whether or not the response was found.
|
|
408
417
|
"""
|
|
409
418
|
# Create a new ZWrapper for the given service
|
|
410
419
|
wrapper = self._get_wrapper(config.service)
|
|
@@ -434,23 +443,41 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
|
|
|
434
443
|
# Now, extract the attachments from the response
|
|
435
444
|
attachments = {a.name: b64decode(a.data) for a in resp.attachments}
|
|
436
445
|
|
|
437
|
-
# Finally, deserialize and verify
|
|
438
|
-
envelope_type = config.response_envelope_type or type(envelope)
|
|
439
|
-
data_type = config.response_data_type or data_type
|
|
446
|
+
# Finally, deserialize the response and verify it
|
|
440
447
|
deserializer = config.serializer or config.service.serializer
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
envelope_type
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
448
|
+
try:
|
|
449
|
+
found = True
|
|
450
|
+
envelope_type = config.response_envelope_type or type(envelope)
|
|
451
|
+
resp_data_type = config.response_data_type or data_type
|
|
452
|
+
data: MultiResponse[E, P] = deserializer.deserialize_multi(
|
|
453
|
+
config.name,
|
|
454
|
+
resp.payload,
|
|
455
|
+
envelope_type,
|
|
456
|
+
resp_data_type, # type: ignore[arg-type]
|
|
457
|
+
config.for_report,
|
|
458
|
+
)
|
|
459
|
+
except EnvelopeNodeNotFoundError:
|
|
460
|
+
logger.warning(
|
|
461
|
+
f"{config.name}: Failed to deserialize multi-response. Attempting to deserialize as the request object."
|
|
462
|
+
)
|
|
463
|
+
found = False
|
|
464
|
+
envelope_type = type(envelope)
|
|
465
|
+
resp_data_type = data_type
|
|
466
|
+
data = deserializer.deserialize_multi(
|
|
467
|
+
config.name,
|
|
468
|
+
resp.payload,
|
|
469
|
+
envelope_type,
|
|
470
|
+
data_type, # type: ignore[arg-type]
|
|
471
|
+
config.for_report,
|
|
472
|
+
)
|
|
447
473
|
self._verify_multi_response(data, config)
|
|
448
474
|
|
|
449
475
|
# Return the response data and any attachments
|
|
450
476
|
logger.debug(
|
|
451
|
-
f"{config.name}: Returning multi-response. Envelope: {envelope_type.__name__}, Data:
|
|
477
|
+
f"{config.name}: Returning multi-response. Envelope: {envelope_type.__name__}, Data: "
|
|
478
|
+
f"{resp_data_type.__name__}"
|
|
452
479
|
)
|
|
453
|
-
return data, attachments
|
|
480
|
+
return data, attachments, found
|
|
454
481
|
|
|
455
482
|
def _to_mms_request(
|
|
456
483
|
self,
|
mms_client/services/market.py
CHANGED
|
@@ -40,7 +40,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
40
40
|
@mms_multi_endpoint(
|
|
41
41
|
name="MarketQuery_ReserveRequirementQuery",
|
|
42
42
|
service=config,
|
|
43
|
-
request_type=RequestType.
|
|
43
|
+
request_type=RequestType.MARKET,
|
|
44
44
|
response_envelope_type=MarketSubmit,
|
|
45
45
|
response_data_type=ReserveRequirement,
|
|
46
46
|
)
|
mms_client/types/award.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
4
|
from enum import Enum
|
|
5
|
+
from typing import Annotated
|
|
5
6
|
from typing import List
|
|
6
7
|
from typing import Optional
|
|
7
8
|
|
|
@@ -323,7 +324,7 @@ class AwardResult(Payload, tag="AwardResults"):
|
|
|
323
324
|
direction: Direction = attr(name="Direction")
|
|
324
325
|
|
|
325
326
|
# The bid awards associated with these parameters
|
|
326
|
-
data: List[Award]
|
|
327
|
+
data: Annotated[List[Award], element(tag="AwardResultsData", min_length=1)]
|
|
327
328
|
|
|
328
329
|
@field_serializer("start", "end")
|
|
329
330
|
def encode_datetime(self, value: DateTime) -> str:
|
|
@@ -340,4 +341,4 @@ class AwardResponse(AwardQuery, tag="AwardResultsQuery"):
|
|
|
340
341
|
"""Contains the results of a bid award query."""
|
|
341
342
|
|
|
342
343
|
# The bid awards associated with the query
|
|
343
|
-
results: Optional[List[AwardResult]]
|
|
344
|
+
results: Annotated[Optional[List[AwardResult]], wrapped(default=None, path="AwardResultsQueryResponse")]
|
mms_client/types/base.py
CHANGED
|
@@ -35,6 +35,9 @@ class Message(BaseXmlModel):
|
|
|
35
35
|
# The message text. Not sure why this is called code in the XML.
|
|
36
36
|
code: str = attr(default="", name="Code", min_length=2, max_length=50, pattern=r"^[a-zA-Z_0-9\-]*$")
|
|
37
37
|
|
|
38
|
+
# A human-readable description of the message
|
|
39
|
+
description: str
|
|
40
|
+
|
|
38
41
|
|
|
39
42
|
class Messages(BaseXmlModel, search_mode="unordered", arbitrary_types_allowed=True):
|
|
40
43
|
"""Represents a collection of messages returned with a payload."""
|
|
@@ -49,21 +52,21 @@ class Messages(BaseXmlModel, search_mode="unordered", arbitrary_types_allowed=Tr
|
|
|
49
52
|
errors_raw: List[Element] = element(default=[], tag="Error", nillable=True, exclude=True)
|
|
50
53
|
|
|
51
54
|
@computed_element
|
|
52
|
-
def information(self) -> List[
|
|
55
|
+
def information(self) -> List[Message]:
|
|
53
56
|
"""Return the information messages."""
|
|
54
57
|
return self._parse_messages(self.information_raw)
|
|
55
58
|
|
|
56
59
|
@computed_element
|
|
57
|
-
def warnings(self) -> List[
|
|
60
|
+
def warnings(self) -> List[Message]:
|
|
58
61
|
"""Return the warning messages."""
|
|
59
62
|
return self._parse_messages(self.warnings_raw)
|
|
60
63
|
|
|
61
64
|
@computed_element
|
|
62
|
-
def errors(self) -> List[
|
|
65
|
+
def errors(self) -> List[Message]:
|
|
63
66
|
"""Return the error messages."""
|
|
64
67
|
return self._parse_messages(self.errors_raw)
|
|
65
68
|
|
|
66
|
-
def _parse_messages(self, raw: List[Element]) -> List[
|
|
69
|
+
def _parse_messages(self, raw: List[Element]) -> List[Message]:
|
|
67
70
|
"""Parse the messages from the XML tree.
|
|
68
71
|
|
|
69
72
|
Arguments:
|
|
@@ -73,10 +76,11 @@ class Messages(BaseXmlModel, search_mode="unordered", arbitrary_types_allowed=Tr
|
|
|
73
76
|
"""
|
|
74
77
|
messages = []
|
|
75
78
|
for item in raw:
|
|
79
|
+
text = item.text or ""
|
|
76
80
|
if message := item.attrib.get("Code"):
|
|
77
|
-
messages.append(message)
|
|
81
|
+
messages.append(Message(description=text, code=message))
|
|
78
82
|
else:
|
|
79
|
-
messages.append(
|
|
83
|
+
messages.append(Message(description=text))
|
|
80
84
|
return messages
|
|
81
85
|
|
|
82
86
|
|
mms_client/types/fields.py
CHANGED
|
@@ -115,7 +115,7 @@ def power_positive(alias: str, optional: bool = False):
|
|
|
115
115
|
|
|
116
116
|
Returns: A Pydantic Field object for the power value.
|
|
117
117
|
"""
|
|
118
|
-
return attr(default=None if optional else PydanticUndefined, name=alias,
|
|
118
|
+
return attr(default=None if optional else PydanticUndefined, name=alias, ge=0, le=10000000)
|
|
119
119
|
|
|
120
120
|
|
|
121
121
|
def price(alias: str, limit: float, optional: bool = False):
|
mms_client/types/offer.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Contains objects for MMS offers."""
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
from typing import List
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
@@ -79,7 +80,7 @@ class OfferData(Payload):
|
|
|
79
80
|
"""Describes the data common to both offer requests and responses."""
|
|
80
81
|
|
|
81
82
|
# The separate offers that make up the offer stack
|
|
82
|
-
stack: List[OfferStack]
|
|
83
|
+
stack: Annotated[List[OfferStack], element(tag="OfferStack", min_length=1, max_length=20)]
|
|
83
84
|
|
|
84
85
|
# The identifier for the power resource being traded
|
|
85
86
|
resource: str = resource_name("ResourceName")
|
mms_client/types/report.py
CHANGED
|
@@ -269,7 +269,7 @@ class NewReportRequest(ReportMetadata, tag="ReportCreateRequest"):
|
|
|
269
269
|
bsp_name: Optional[str] = participant("BSPName", True)
|
|
270
270
|
|
|
271
271
|
# Parameters to be use when configuring the report
|
|
272
|
-
parameters: List[Parameter]
|
|
272
|
+
parameters: Annotated[List[Parameter], element(tag="Param", max_length=5)]
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
class NewReportResponse(NewReportRequest, tag="ReportCreateRequest"):
|
|
@@ -287,7 +287,7 @@ class ListReportResponse(ListReportRequest, tag="ReportListRequest"):
|
|
|
287
287
|
"""Represents the base fields for a list report response."""
|
|
288
288
|
|
|
289
289
|
# The list of reports that match the query
|
|
290
|
-
reports: List[ReportItem]
|
|
290
|
+
reports: Annotated[List[ReportItem], wrapped(path="ReportListResponse", entity=element(tag="ReportItem"))]
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
class ReportDownloadRequest(ReportData):
|
mms_client/types/reserve.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Contains objects for MMS reserve requirements."""
|
|
2
2
|
|
|
3
|
+
from typing import Annotated
|
|
3
4
|
from typing import List
|
|
4
5
|
from typing import Optional
|
|
5
6
|
|
|
@@ -71,7 +72,7 @@ class ReserveRequirement(Payload):
|
|
|
71
72
|
area: AreaCode = attr(name="Area")
|
|
72
73
|
|
|
73
74
|
# The requirements associated with the area
|
|
74
|
-
requirements: List[Requirement]
|
|
75
|
+
requirements: Annotated[List[Requirement], element(tag="Requirement", min_length=1)]
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
class ReserveRequirementQuery(Payload):
|
mms_client/types/resource.py
CHANGED
|
@@ -328,7 +328,7 @@ class StartupPattern(Payload, tag="StartupPatternInfo"):
|
|
|
328
328
|
pattern_name: str = pattern_name("PatternName")
|
|
329
329
|
|
|
330
330
|
# The events associated with this startup pattern
|
|
331
|
-
events: List[StartupEvent]
|
|
331
|
+
events: Annotated[List[StartupEvent], element(tag="StartupPatternEvent", min_length=6, max_length=25)]
|
|
332
332
|
|
|
333
333
|
|
|
334
334
|
class ShutdownEvent(Payload):
|
|
@@ -366,34 +366,36 @@ class ShutdownPattern(Payload, tag="StopPatternInfo"):
|
|
|
366
366
|
pattern_name: str = pattern_name("PatternName")
|
|
367
367
|
|
|
368
368
|
# The events associated with this stop pattern
|
|
369
|
-
events: List[ShutdownEvent]
|
|
369
|
+
events: Annotated[List[ShutdownEvent], element(tag="StopPatternEvent", min_length=2, max_length=21)]
|
|
370
370
|
|
|
371
371
|
|
|
372
372
|
class ResourceData(Payload, tag="Resource"):
|
|
373
373
|
"""Contains the data common to both resource requests and responses."""
|
|
374
374
|
|
|
375
375
|
# The output bands associated with this resource
|
|
376
|
-
output_bands:
|
|
376
|
+
output_bands: Annotated[
|
|
377
|
+
Optional[List[OutputBand]], wrapped(default=None, path="OutputBand", min_length=1, max_length=20)
|
|
378
|
+
]
|
|
377
379
|
|
|
378
380
|
# The switching outputs associated with this resource
|
|
379
|
-
switch_outputs:
|
|
380
|
-
default=None, path="SwitchOutput", min_length=1, max_length=20
|
|
381
|
-
|
|
381
|
+
switch_outputs: Annotated[
|
|
382
|
+
Optional[List[SwitchOutput]], wrapped(default=None, path="SwitchOutput", min_length=1, max_length=20)
|
|
383
|
+
]
|
|
382
384
|
|
|
383
385
|
# The minimum EDC/LFC outputs associated with this resource
|
|
384
|
-
afc_minimum_outputs:
|
|
385
|
-
default=None, path="OutputRangeBelowAfc", min_length=1, max_length=20
|
|
386
|
-
|
|
386
|
+
afc_minimum_outputs: Annotated[
|
|
387
|
+
Optional[List[AfcMinimumOutput]], wrapped(default=None, path="OutputRangeBelowAfc", min_length=1, max_length=20)
|
|
388
|
+
]
|
|
387
389
|
|
|
388
390
|
# The startup patterns associated with this resource
|
|
389
|
-
startup_patterns:
|
|
390
|
-
default=None, path="StartupPattern", min_length=1, max_length=20
|
|
391
|
-
|
|
391
|
+
startup_patterns: Annotated[
|
|
392
|
+
Optional[List[StartupPattern]], wrapped(default=None, path="StartupPattern", min_length=1, max_length=20)
|
|
393
|
+
]
|
|
392
394
|
|
|
393
395
|
# The stop patterns associated with this resource
|
|
394
|
-
shutdown_patterns:
|
|
395
|
-
default=None, path="StopPattern", min_length=1, max_length=20
|
|
396
|
-
|
|
396
|
+
shutdown_patterns: Annotated[
|
|
397
|
+
Optional[List[ShutdownPattern]], wrapped(default=None, path="StopPattern", min_length=1, max_length=20)
|
|
398
|
+
]
|
|
397
399
|
|
|
398
400
|
# Any comments attached to the resource
|
|
399
401
|
comments: Optional[str] = element(
|
mms_client/types/transport.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Contains the base types necessary for communication with the MMS server."""
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
from typing import List
|
|
5
6
|
from typing import Optional
|
|
6
7
|
|
|
@@ -87,7 +88,7 @@ class MmsRequest(BaseModel):
|
|
|
87
88
|
|
|
88
89
|
# Any attached files to be sent with the request. Only 20 of these are allowed for OMI requests. For MI requests,
|
|
89
90
|
# the limit is 40.
|
|
90
|
-
attachments: List[Attachment]
|
|
91
|
+
attachments: Annotated[List[Attachment], Field(alias="attachmentData")] = []
|
|
91
92
|
|
|
92
93
|
def to_arguments(self) -> dict:
|
|
93
94
|
"""Convert the request to a dictionary of arguments for use in the MMS client."""
|
|
@@ -127,4 +128,4 @@ class MmsResponse(BaseModel):
|
|
|
127
128
|
payload: bytes = Field(alias="responseData")
|
|
128
129
|
|
|
129
130
|
# Any attached files to be sent with the response
|
|
130
|
-
attachments: List[Attachment]
|
|
131
|
+
attachments: Annotated[List[Attachment], Field(alias="attachmentData")] = []
|
mms_client/utils/errors.py
CHANGED
|
@@ -4,6 +4,7 @@ from logging import getLogger
|
|
|
4
4
|
from typing import Dict
|
|
5
5
|
from typing import List
|
|
6
6
|
from typing import Optional
|
|
7
|
+
from typing import Type
|
|
7
8
|
from typing import Union
|
|
8
9
|
|
|
9
10
|
from mms_client.types.base import E
|
|
@@ -88,3 +89,53 @@ class MMSValidationError(RuntimeError):
|
|
|
88
89
|
self.method = method
|
|
89
90
|
self.messages = messages
|
|
90
91
|
super().__init__(self.message)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class InvalidContainerError(ValueError):
|
|
95
|
+
"""Error raised when the outer XML tag is not the expected one."""
|
|
96
|
+
|
|
97
|
+
def __init__(self, method: str, expected: str, actual: str):
|
|
98
|
+
"""Initialize the error.
|
|
99
|
+
|
|
100
|
+
Arguments:
|
|
101
|
+
method (str): The method that caused the error.
|
|
102
|
+
expected (str): The expected outer XML tag.
|
|
103
|
+
actual (str): The actual outer XML tag.
|
|
104
|
+
"""
|
|
105
|
+
self.message = f"{method}: Expected payload key '{expected}' in response, but found '{actual}'."
|
|
106
|
+
self.method = method
|
|
107
|
+
self.expected = expected
|
|
108
|
+
self.actual = actual
|
|
109
|
+
super().__init__(self.message)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class EnvelopeNodeNotFoundError(ValueError):
|
|
113
|
+
"""Error raised when the envelope node is not found."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, method: str, expected: str):
|
|
116
|
+
"""Initialize the error.
|
|
117
|
+
|
|
118
|
+
Arguments:
|
|
119
|
+
method (str): The method that caused the error.
|
|
120
|
+
expected (str): The expected envelope XML tag.
|
|
121
|
+
"""
|
|
122
|
+
self.message = f"{method}: Expected envelope node '{expected}' not found in response."
|
|
123
|
+
self.method = method
|
|
124
|
+
self.expected = expected
|
|
125
|
+
super().__init__(self.message)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DataNodeNotFoundError(ValueError):
|
|
129
|
+
"""Error raised when the data node is not found."""
|
|
130
|
+
|
|
131
|
+
def __init__(self, method: str, expected: Type):
|
|
132
|
+
"""Initialize the error.
|
|
133
|
+
|
|
134
|
+
Arguments:
|
|
135
|
+
method (str): The method that caused the error.
|
|
136
|
+
expected (Type): The expected data node.
|
|
137
|
+
"""
|
|
138
|
+
self.message = f"{method}: Expected data node '{expected.__name__}' not found in response."
|
|
139
|
+
self.method = method
|
|
140
|
+
self.expected = expected
|
|
141
|
+
super().__init__(self.message)
|
|
@@ -29,6 +29,9 @@ from mms_client.types.base import Response
|
|
|
29
29
|
from mms_client.types.base import ResponseCommon
|
|
30
30
|
from mms_client.types.base import ResponseData
|
|
31
31
|
from mms_client.types.base import SchemaType
|
|
32
|
+
from mms_client.utils.errors import DataNodeNotFoundError
|
|
33
|
+
from mms_client.utils.errors import EnvelopeNodeNotFoundError
|
|
34
|
+
from mms_client.utils.errors import InvalidContainerError
|
|
32
35
|
|
|
33
36
|
# Directory containing all our XML schemas
|
|
34
37
|
XSD_DIR = Path(__file__).parent.parent / "schemas" / "xsd"
|
|
@@ -115,11 +118,12 @@ class Serializer:
|
|
|
115
118
|
return self._to_canoncialized_xml(payload)
|
|
116
119
|
|
|
117
120
|
def deserialize(
|
|
118
|
-
self, data: bytes, envelope_type: Type[E], data_type: Type[P], for_report: bool = False
|
|
121
|
+
self, method: str, data: bytes, envelope_type: Type[E], data_type: Type[P], for_report: bool = False
|
|
119
122
|
) -> Response[E, P]:
|
|
120
123
|
"""Deserialize the data to a response object.
|
|
121
124
|
|
|
122
125
|
Arguments:
|
|
126
|
+
method (str): The method for which the data was received.
|
|
123
127
|
data (bytes): The raw data to be deserialized.
|
|
124
128
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
125
129
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
@@ -128,14 +132,15 @@ class Serializer:
|
|
|
128
132
|
Returns: A response object containing the envelope and data extracted from the raw data.
|
|
129
133
|
"""
|
|
130
134
|
tree = self._from_xml(data)
|
|
131
|
-
return self._from_tree(tree, envelope_type, data_type, for_report)
|
|
135
|
+
return self._from_tree(method, tree, envelope_type, data_type, for_report)
|
|
132
136
|
|
|
133
137
|
def deserialize_multi(
|
|
134
|
-
self, data: bytes, envelope_type: Type[E], data_type: Type[P], for_report: bool = False
|
|
138
|
+
self, method: str, data: bytes, envelope_type: Type[E], data_type: Type[P], for_report: bool = False
|
|
135
139
|
) -> MultiResponse[E, P]:
|
|
136
140
|
"""Deserialize the data to a multi-response object.
|
|
137
141
|
|
|
138
142
|
Arguments:
|
|
143
|
+
method (str): The method for which the data was received.
|
|
139
144
|
data (bytes): The raw data to be deserialized.
|
|
140
145
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
141
146
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
@@ -144,7 +149,7 @@ class Serializer:
|
|
|
144
149
|
Returns: A multi-response object containing the envelope and data extracted from the raw data.
|
|
145
150
|
"""
|
|
146
151
|
tree = self._from_xml(data)
|
|
147
|
-
return self._from_tree_multi(tree, envelope_type, data_type, for_report)
|
|
152
|
+
return self._from_tree_multi(method, tree, envelope_type, data_type, for_report)
|
|
148
153
|
|
|
149
154
|
def _to_canoncialized_xml(self, payload: PayloadBase) -> bytes:
|
|
150
155
|
"""Convert the payload to a canonicalized XML string.
|
|
@@ -170,10 +175,13 @@ class Serializer:
|
|
|
170
175
|
buffer.seek(0)
|
|
171
176
|
return buffer.read()
|
|
172
177
|
|
|
173
|
-
def _from_tree(
|
|
178
|
+
def _from_tree(
|
|
179
|
+
self, method: str, raw: Element, envelope_type: Type[E], data_type: Type[P], for_report: bool
|
|
180
|
+
) -> Response[E, P]:
|
|
174
181
|
"""Convert the raw data to a response object.
|
|
175
182
|
|
|
176
183
|
Arguments:
|
|
184
|
+
method (str): The method for which the data was received.
|
|
177
185
|
raw (Element): The raw data to be converted.
|
|
178
186
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
179
187
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
@@ -184,7 +192,7 @@ class Serializer:
|
|
|
184
192
|
# First, attempt to extract the response from the raw data; if the key isn't found then we'll raise an error.
|
|
185
193
|
# Otherwise, we'll attempt to construct the response from the raw data.
|
|
186
194
|
if self._payload_key != raw.tag:
|
|
187
|
-
raise
|
|
195
|
+
raise InvalidContainerError(method, self._payload_key, raw.tag)
|
|
188
196
|
cls: Response[E, P] = _create_response_payload_type( # type: ignore[assignment]
|
|
189
197
|
self._payload_key,
|
|
190
198
|
envelope_type, # type: ignore[arg-type]
|
|
@@ -195,12 +203,12 @@ class Serializer:
|
|
|
195
203
|
|
|
196
204
|
# Next, attempt to extract the envelope and data from within the response
|
|
197
205
|
resp.envelope, resp.envelope_validation, envelope_node = self._from_tree_envelope(
|
|
198
|
-
raw, envelope_type, for_report
|
|
206
|
+
method, raw, envelope_type, for_report
|
|
199
207
|
)
|
|
200
208
|
|
|
201
209
|
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
202
210
|
# from within the envelope
|
|
203
|
-
self._verify_tree_data_tag(envelope_node, data_type)
|
|
211
|
+
self._verify_tree_data_tag(method, envelope_node, data_type)
|
|
204
212
|
resp.payload = self._from_tree_data(envelope_node.find(get_tag(data_type)), data_type)
|
|
205
213
|
|
|
206
214
|
# Finally, attempt to extract the messages from within the payload
|
|
@@ -212,11 +220,12 @@ class Serializer:
|
|
|
212
220
|
return resp
|
|
213
221
|
|
|
214
222
|
def _from_tree_multi(
|
|
215
|
-
self, raw: Element, envelope_type: Type[E], data_type: Type[P], for_report: bool
|
|
223
|
+
self, method: str, raw: Element, envelope_type: Type[E], data_type: Type[P], for_report: bool
|
|
216
224
|
) -> MultiResponse[E, P]:
|
|
217
225
|
"""Convert the raw data to a multi-response object.
|
|
218
226
|
|
|
219
227
|
Arguments:
|
|
228
|
+
method (str): The method for which the data was received.
|
|
220
229
|
raw (Element): The raw data to be converted.
|
|
221
230
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
222
231
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
@@ -227,7 +236,7 @@ class Serializer:
|
|
|
227
236
|
# First, attempt to extract the response from the raw data; if the key isn't found then we'll raise an error.
|
|
228
237
|
# Otherwise, we'll attempt to construct the response from the raw data.
|
|
229
238
|
if self._payload_key != raw.tag:
|
|
230
|
-
raise
|
|
239
|
+
raise InvalidContainerError(method, self._payload_key, raw.tag)
|
|
231
240
|
cls: MultiResponse[E, P] = _create_response_payload_type( # type: ignore[assignment]
|
|
232
241
|
self._payload_key,
|
|
233
242
|
envelope_type, # type: ignore[arg-type]
|
|
@@ -237,12 +246,14 @@ class Serializer:
|
|
|
237
246
|
resp = cls.from_xml_tree(raw) # type: ignore[arg-type]
|
|
238
247
|
|
|
239
248
|
# Next, attempt to extract the envelope from the response
|
|
240
|
-
resp.envelope, resp.envelope_validation, env_node = self._from_tree_envelope(
|
|
249
|
+
resp.envelope, resp.envelope_validation, env_node = self._from_tree_envelope(
|
|
250
|
+
method, raw, envelope_type, for_report
|
|
251
|
+
)
|
|
241
252
|
|
|
242
253
|
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
243
254
|
# from within the envelope
|
|
244
255
|
# NOTE: apparently, mypy doesn't know about setter-getter properties either...
|
|
245
|
-
self._verify_tree_data_tag(env_node, data_type)
|
|
256
|
+
self._verify_tree_data_tag(method, env_node, data_type)
|
|
246
257
|
resp.payload = [
|
|
247
258
|
self._from_tree_data(item, data_type) for item in env_node.findall(get_tag(data_type)) # type: ignore[misc]
|
|
248
259
|
]
|
|
@@ -256,11 +267,12 @@ class Serializer:
|
|
|
256
267
|
return resp
|
|
257
268
|
|
|
258
269
|
def _from_tree_envelope(
|
|
259
|
-
self, raw: Element, envelope_type: Type[E], for_report: bool
|
|
270
|
+
self, method: str, raw: Element, envelope_type: Type[E], for_report: bool
|
|
260
271
|
) -> Tuple[E, ResponseCommon, Element]:
|
|
261
272
|
"""Attempt to extract the envelope from within the response.
|
|
262
273
|
|
|
263
274
|
Arguments:
|
|
275
|
+
method (str): The method for which the data was received.
|
|
264
276
|
raw (Element): The raw data to be converted.
|
|
265
277
|
envelope_type (Type[Envelope]): The type of envelope to be constructed.
|
|
266
278
|
for_report (bool): If True, the data will be serialized for a report request.
|
|
@@ -274,7 +286,7 @@ class Serializer:
|
|
|
274
286
|
envelope_tag = get_tag(envelope_type)
|
|
275
287
|
envelope_node = raw if for_report else raw.find(envelope_tag)
|
|
276
288
|
if envelope_node is None or envelope_node.tag != envelope_tag:
|
|
277
|
-
raise
|
|
289
|
+
raise EnvelopeNodeNotFoundError(method, envelope_tag)
|
|
278
290
|
|
|
279
291
|
# Next, create a new envelope type that contains the envelope type with the appropriate XML tag. We have to do
|
|
280
292
|
# this because the envelope type doesn't include the ResponseCommon fields, and the tag doesn't match
|
|
@@ -288,10 +300,11 @@ class Serializer:
|
|
|
288
300
|
envelope_node,
|
|
289
301
|
)
|
|
290
302
|
|
|
291
|
-
def _verify_tree_data_tag(self, raw: Element, data_type: Type[P]) -> None:
|
|
303
|
+
def _verify_tree_data_tag(self, method: str, raw: Element, data_type: Type[P]) -> None:
|
|
292
304
|
"""Verify that no types other than the expected data type are present in the response.
|
|
293
305
|
|
|
294
306
|
Arguments:
|
|
307
|
+
method (str): The method for which the data was received.
|
|
295
308
|
raw (Element): The raw data to be converted.
|
|
296
309
|
data_type (Type[Payload]): The type of data to be constructed.
|
|
297
310
|
|
|
@@ -300,7 +313,7 @@ class Serializer:
|
|
|
300
313
|
"""
|
|
301
314
|
data_tags = set(node.tag for node in raw)
|
|
302
315
|
if not data_tags.issubset([data_type.__name__, data_type.__xml_tag__, "ProcessingStatistics", "Messages"]):
|
|
303
|
-
raise
|
|
316
|
+
raise DataNodeNotFoundError(method, data_type)
|
|
304
317
|
|
|
305
318
|
def _from_tree_data(self, raw: Optional[Element], data_type: Type[P]) -> Optional[ResponseData[P]]:
|
|
306
319
|
"""Attempt to extract the data from within the payload.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mms-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.10.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
|
|
@@ -80,6 +80,8 @@ This object represents the top-level XML element contained within the `MmsRespon
|
|
|
80
80
|
### Response & MultiResponse
|
|
81
81
|
These objects contain the actual payload data and inherit from `BaseResponse`. These are what will actually be returned from the deserialization process. They also contain validation data for the top-level paylaod item(s). The difference between `Response` and `MultiResponse` is that the former contains a single item and the latter contains a list.
|
|
82
82
|
|
|
83
|
+
Note that the `MultiResponse` object covers a special case where queries made to the MMS may return no items, in which case the response will be the request object itself. This is handled internally by the client, and the user will receive an empty list in such cases.
|
|
84
|
+
|
|
83
85
|
## Envelopes
|
|
84
86
|
Not to be confused with the SOAP envelope, this envelope contains the method parameters used to send requests to the MMS server. For example, if you wanted to send a market-related request, this would take on the form of a `MarketQuery`, `MarketSubmit` or `MarketCancel` object. This is combined with the payload during the serialization process to produce the final XML payload before injecting it into the `MmsRequest`. During the deserialization process, this is extracted from the XML paylod on the `MmsResponse` object. Each of these should inherit from `mms_client.types.base.Envelope`.
|
|
85
87
|
|
|
@@ -12,30 +12,30 @@ mms_client/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
12
12
|
mms_client/security/certs.py,sha256=Gy-CuSsdLPFeoPH_sEYhY67dI5sy6yJ8iTwlysRKT1s,3018
|
|
13
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=1CJ528X2TJgG3uKa1_txjRHuTOQGQxsm43iFWfrvXBs,31643
|
|
16
|
+
mms_client/services/market.py,sha256=qzTQWPgXcI2uvDxC0SnYL9pyWJ4TolFGhbkgb4jS8gA,9549
|
|
17
17
|
mms_client/services/omi.py,sha256=UG1zYkFz0sFsEbhE6P0CLoAOZZOyEshkZ_b7D_e3CjQ,626
|
|
18
18
|
mms_client/services/registration.py,sha256=XkXBgPK9PXFyNxo3ways_hUJ3E_vKTUlgCPgQQbDgWM,3839
|
|
19
19
|
mms_client/services/report.py,sha256=hgRvjVxy8Hvbj5hQb0GMyDW0-1kMW2XFS5iKfO90YZY,6258
|
|
20
20
|
mms_client/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
mms_client/types/award.py,sha256=
|
|
22
|
-
mms_client/types/base.py,sha256=
|
|
21
|
+
mms_client/types/award.py,sha256=1-CHIE3x5zFWA8WccUkGwQFBpLdsm4VQ7rk8uOoyKVc,15771
|
|
22
|
+
mms_client/types/base.py,sha256=Y0CZEWPQgkPXZWz_WXOpW2J8mRryCRNU7oAZEdWkqvQ,11405
|
|
23
23
|
mms_client/types/enums.py,sha256=hALMuqRChLUJ1Eglll5R5mkYLpcO-ZIaEBps3MjkTg0,2534
|
|
24
|
-
mms_client/types/fields.py,sha256=
|
|
24
|
+
mms_client/types/fields.py,sha256=kPHN_5TT5sSarkic56Tvr2k-fTvf8oFj53DXGwRfrPg,14785
|
|
25
25
|
mms_client/types/market.py,sha256=D5tZB97ewHfwG9gVC35bHfdlGthi5GBeMaGBk5393wY,2577
|
|
26
|
-
mms_client/types/offer.py,sha256=
|
|
26
|
+
mms_client/types/offer.py,sha256=Dtx7ftLgaJONVEetalXaXmJM4bJ65NBMFFNTGwgLF90,7960
|
|
27
27
|
mms_client/types/registration.py,sha256=bPA_FQwLVIwb5CRqK8F3YqeV0dEeN04j1wMsClrV5Wc,1252
|
|
28
|
-
mms_client/types/report.py,sha256=
|
|
29
|
-
mms_client/types/reserve.py,sha256=
|
|
30
|
-
mms_client/types/resource.py,sha256=
|
|
31
|
-
mms_client/types/transport.py,sha256
|
|
28
|
+
mms_client/types/report.py,sha256=7ENst_h1V2DG342UwL9ZltdRK2TaI-P1QW26qwjb6ao,23687
|
|
29
|
+
mms_client/types/reserve.py,sha256=nsFJ0oSEuoCf82CtZEFa_-1UKAj-5Ssf1gEoWTlYiCI,3296
|
|
30
|
+
mms_client/types/resource.py,sha256=4WhrSKOAXGrkg04r9f8f5kC3r0XDIKLarOFtBd2f7NI,65384
|
|
31
|
+
mms_client/types/transport.py,sha256=4iEYod4DbqBBRXMH_VBqK9ORwR2q7MxSwwfWQAeHcT4,4442
|
|
32
32
|
mms_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
|
-
mms_client/utils/errors.py,sha256=
|
|
33
|
+
mms_client/utils/errors.py,sha256=aDpAqI3Y4D3DtgsCtaaLmla_iOoIZ0SjpoYjzymCGMI,4736
|
|
34
34
|
mms_client/utils/multipart_transport.py,sha256=GJvjdlmXituiT78f5XuhpPkcHdDHFtVELa-F3eqUvbk,9981
|
|
35
35
|
mms_client/utils/plugin.py,sha256=_Jymcny5ta9uV4CMLGDX7O5xSQIhuu76rb-A6uhtFSY,2013
|
|
36
|
-
mms_client/utils/serialization.py,sha256=
|
|
36
|
+
mms_client/utils/serialization.py,sha256=xF-C8Owp5HE6xfdcT00HFZXZnF-9vljIxoWNyjPHK5U,34512
|
|
37
37
|
mms_client/utils/web.py,sha256=Qk8azZpxAIEtI9suOikxBNtFQFNuWh-92DaUBU1qX8s,10927
|
|
38
|
-
mms_client-1.
|
|
39
|
-
mms_client-1.
|
|
40
|
-
mms_client-1.
|
|
41
|
-
mms_client-1.
|
|
38
|
+
mms_client-1.10.0.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
39
|
+
mms_client-1.10.0.dist-info/METADATA,sha256=Z8-YF70jaXeZLzbiNcGifOYnW0aSnJqnT3I1tNkAbDU,16849
|
|
40
|
+
mms_client-1.10.0.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
41
|
+
mms_client-1.10.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|