mms-client 1.9.2__tar.gz → 1.10.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 (41) hide show
  1. {mms_client-1.9.2 → mms_client-1.10.0}/PKG-INFO +3 -1
  2. {mms_client-1.9.2 → mms_client-1.10.0}/README.md +2 -0
  3. {mms_client-1.9.2 → mms_client-1.10.0}/pyproject.toml +1 -1
  4. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/services/base.py +47 -20
  5. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/services/market.py +1 -1
  6. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/award.py +3 -2
  7. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/base.py +10 -6
  8. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/fields.py +1 -1
  9. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/offer.py +2 -1
  10. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/report.py +2 -2
  11. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/reserve.py +2 -1
  12. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/resource.py +17 -15
  13. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/transport.py +3 -2
  14. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/utils/errors.py +51 -0
  15. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/utils/serialization.py +29 -16
  16. {mms_client-1.9.2 → mms_client-1.10.0}/LICENSE +0 -0
  17. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/__init__.py +0 -0
  18. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/client.py +0 -0
  19. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/py.typed +0 -0
  20. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/wsdl/mi-web-service-jbms.wsdl +0 -0
  21. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/wsdl/omi-web-service.wsdl +0 -0
  22. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/xsd/mi-market.xsd +0 -0
  23. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/xsd/mi-outbnd-reports.xsd +0 -0
  24. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/xsd/mi-report.xsd +0 -0
  25. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/xsd/mpr.xsd +0 -0
  26. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/schemas/xsd/omi.xsd +0 -0
  27. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/security/__init__.py +0 -0
  28. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/security/certs.py +0 -0
  29. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/security/crypto.py +0 -0
  30. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/services/__init__.py +0 -0
  31. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/services/omi.py +0 -0
  32. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/services/registration.py +0 -0
  33. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/services/report.py +0 -0
  34. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/__init__.py +0 -0
  35. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/enums.py +0 -0
  36. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/market.py +0 -0
  37. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/types/registration.py +0 -0
  38. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/utils/__init__.py +0 -0
  39. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/utils/multipart_transport.py +0 -0
  40. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/utils/plugin.py +0 -0
  41. {mms_client-1.9.2 → mms_client-1.10.0}/src/mms_client/utils/web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mms-client
3
- Version: 1.9.2
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
 
@@ -46,6 +46,8 @@ This object represents the top-level XML element contained within the `MmsRespon
46
46
  ### Response & MultiResponse
47
47
  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.
48
48
 
49
+ 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.
50
+
49
51
  ## Envelopes
50
52
  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`.
51
53
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "mms_client"
3
- version = "v1.9.2"
3
+ version = "v1.10.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"
@@ -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: The multi-response from the MMS server.
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(resp.payload, envelope_type, data_type, config.for_report)
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: The multi-response from the MMS server.
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 the response
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
- data: MultiResponse[E, P] = deserializer.deserialize_multi(
442
- resp.payload,
443
- envelope_type,
444
- data_type, # type: ignore[arg-type]
445
- config.for_report,
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: {data_type.__name__}",
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,
@@ -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.INFO,
43
+ request_type=RequestType.MARKET,
44
44
  response_envelope_type=MarketSubmit,
45
45
  response_data_type=ReserveRequirement,
46
46
  )
@@ -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] = element(tag="AwardResultsData", min_length=1)
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]] = wrapped(default=None, path="AwardResultsQueryResponse")
344
+ results: Annotated[Optional[List[AwardResult]], wrapped(default=None, path="AwardResultsQueryResponse")]
@@ -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[str]:
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[str]:
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[str]:
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[str]:
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(item.text or "")
83
+ messages.append(Message(description=text))
80
84
  return messages
81
85
 
82
86
 
@@ -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, gt=0, le=10000000)
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):
@@ -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] = element(tag="OfferStack", min_length=1, max_length=20)
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")
@@ -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] = element(tag="Param", max_length=5)
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] = wrapped(path="ReportListResponse", entity=element(tag="ReportItem"))
290
+ reports: Annotated[List[ReportItem], wrapped(path="ReportListResponse", entity=element(tag="ReportItem"))]
291
291
 
292
292
 
293
293
  class ReportDownloadRequest(ReportData):
@@ -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] = element(tag="Requirement", min_length=1)
75
+ requirements: Annotated[List[Requirement], element(tag="Requirement", min_length=1)]
75
76
 
76
77
 
77
78
  class ReserveRequirementQuery(Payload):
@@ -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] = element(tag="StartupPatternEvent", min_length=6, max_length=25)
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] = element(tag="StopPatternEvent", min_length=2, max_length=21)
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: Optional[List[OutputBand]] = wrapped(default=None, path="OutputBand", min_length=1, max_length=20)
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: Optional[List[SwitchOutput]] = wrapped(
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: Optional[List[AfcMinimumOutput]] = wrapped(
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: Optional[List[StartupPattern]] = wrapped(
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: Optional[List[ShutdownPattern]] = wrapped(
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(
@@ -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] = Field(default=[], alias="attachmentData")
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] = Field(default=[], alias="attachmentData")
131
+ attachments: Annotated[List[Attachment], Field(alias="attachmentData")] = []
@@ -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(self, raw: Element, envelope_type: Type[E], data_type: Type[P], for_report: bool) -> Response[E, P]:
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 ValueError(f"Expected payload key '{self._payload_key}' not found in response")
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 ValueError(f"Expected payload key '{self._payload_key}' not found in response")
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(raw, envelope_type, for_report)
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 ValueError(f"Expected envelope type '{envelope_tag}' not found in response")
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 ValueError(f"Expected data type '{data_type.__name__}' not found in response")
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.
File without changes