mms-client 1.9.3__py3-none-any.whl → 1.11.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.
@@ -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,14 +410,17 @@ 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)
411
420
 
412
421
  # First, create the MMS request from the payload and data.
413
422
  is_list = isinstance(payload, list)
414
- data_type = type(payload[0]) if is_list else type(payload) # type: ignore[index]
423
+ data_type: type = type(payload[0]) if is_list else type(payload) # type: ignore[index]
415
424
  logger.debug(
416
425
  (
417
426
  f"{config.name}: Starting multi-request. Envelope: {type(envelope).__name__}, "
@@ -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,
@@ -12,6 +12,9 @@ from mms_client.services.base import mms_endpoint
12
12
  from mms_client.services.base import mms_multi_endpoint
13
13
  from mms_client.types.award import AwardQuery
14
14
  from mms_client.types.award import AwardResponse
15
+ from mms_client.types.bup import BalancingUnitPriceQuery
16
+ from mms_client.types.bup import BalancingUnitPriceSubmit
17
+ from mms_client.types.market import Defaults
15
18
  from mms_client.types.market import MarketCancel
16
19
  from mms_client.types.market import MarketQuery
17
20
  from mms_client.types.market import MarketSubmit
@@ -21,6 +24,8 @@ from mms_client.types.offer import OfferData
21
24
  from mms_client.types.offer import OfferQuery
22
25
  from mms_client.types.reserve import ReserveRequirement
23
26
  from mms_client.types.reserve import ReserveRequirementQuery
27
+ from mms_client.types.settlement import SettlementQuery
28
+ from mms_client.types.settlement import SettlementResults
24
29
  from mms_client.types.transport import RequestType
25
30
  from mms_client.utils.serialization import SchemaType
26
31
  from mms_client.utils.serialization import Serializer
@@ -40,7 +45,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
40
45
  @mms_multi_endpoint(
41
46
  name="MarketQuery_ReserveRequirementQuery",
42
47
  service=config,
43
- request_type=RequestType.INFO,
48
+ request_type=RequestType.MARKET,
44
49
  response_envelope_type=MarketSubmit,
45
50
  response_data_type=ReserveRequirement,
46
51
  )
@@ -213,3 +218,120 @@ class MarketClientMixin: # pylint: disable=unused-argument
213
218
  user=self.user,
214
219
  days=days,
215
220
  )
221
+
222
+ @mms_endpoint(
223
+ name="MarketQuery_SettlementResultsFileListQuery",
224
+ service=config,
225
+ request_type=RequestType.MARKET,
226
+ response_envelope_type=MarketSubmit,
227
+ response_data_type=SettlementResults,
228
+ allowed_clients=[ClientType.BSP, ClientType.TSO],
229
+ )
230
+ def get_settlement_results(
231
+ self: ClientProto, request: SettlementQuery, days: int, date: Optional[Date] = None
232
+ ) -> SettlementResults:
233
+ """Query the MMS server for settlement results.
234
+
235
+ This endpoint is only accessible to BSPs and TSOs.
236
+
237
+ Arguments:
238
+ request (SettlementQuery): The query to submit to the MMS server.
239
+ days (int): The number of days ahead for which the data is being queried.
240
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to the
241
+ current date.
242
+
243
+ Returns: The settlement results that match the query.
244
+ """
245
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
246
+ return MarketQuery( # type: ignore[return-value]
247
+ date=date or Date.today(),
248
+ participant=self.participant,
249
+ user=self.user,
250
+ days=days,
251
+ )
252
+
253
+ @mms_multi_endpoint(
254
+ name="MarketSubmit_BupSubmit", service=config, request_type=RequestType.MARKET, allowed_clients=[ClientType.BSP]
255
+ )
256
+ def put_bups(
257
+ self: ClientProto,
258
+ requests: List[BalancingUnitPriceSubmit],
259
+ date: Optional[Date] = None,
260
+ default: bool = False,
261
+ ) -> List[BalancingUnitPriceSubmit]:
262
+ """Submit multiple balancing unit prices to the MMS server.
263
+
264
+ This endpoint is only accessible to BSPs.
265
+
266
+ Arguments:
267
+ requests (List[BalancingUnitPriceSubmit]): The balancing unit prices to submit to the MMS server.
268
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value
269
+ defaults to the current date.
270
+ default (bool): Whether or not the balancing unit prices are the default.
271
+
272
+ Returns: A list of balancing unit prices that have been registered with the MMS server.
273
+ """
274
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
275
+ return MarketSubmit( # type: ignore[return-value]
276
+ date=date or Date.today(),
277
+ participant=self.participant,
278
+ user=self.user,
279
+ defaults=Defaults(is_default=default),
280
+ )
281
+
282
+ @mms_endpoint(
283
+ name="MarketSubmit_BupSubmit", service=config, request_type=RequestType.MARKET, allowed_clients=[ClientType.BSP]
284
+ )
285
+ def put_bup(
286
+ self: ClientProto,
287
+ request: BalancingUnitPriceSubmit,
288
+ date: Optional[Date] = None,
289
+ default: bool = False,
290
+ ) -> BalancingUnitPriceSubmit:
291
+ """Submit a balancing unit price to the MMS server.
292
+
293
+ This endpoint is only accessible to BSPs.
294
+
295
+ Arguments:
296
+ request (BalancingUnitPriceSubmit): The balancing unit price to submit to the MMS server.
297
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults
298
+ to the current date.
299
+ default (bool): Whether or not the balancing unit price is the default.
300
+
301
+ Returns: The balancing unit price that has been registered with the MMS server.
302
+ """
303
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
304
+ return MarketSubmit( # type: ignore[return-value]
305
+ date=date or Date.today(),
306
+ participant=self.participant,
307
+ user=self.user,
308
+ defaults=Defaults(is_default=default),
309
+ )
310
+
311
+ @mms_multi_endpoint(
312
+ name="MarketQuery_BupQuery",
313
+ service=config,
314
+ request_type=RequestType.MARKET,
315
+ response_envelope_type=MarketSubmit,
316
+ response_data_type=BalancingUnitPriceSubmit,
317
+ )
318
+ def query_bups(
319
+ self: ClientProto, request: BalancingUnitPriceQuery, date: Optional[Date] = None
320
+ ) -> List[BalancingUnitPriceSubmit]:
321
+ """Query the MMS server for balancing unit prices.
322
+
323
+ This endpoint is accessible to all client types.
324
+
325
+ Arguments:
326
+ request (BalancingUnitPriceSubmit): The query to submit to the MMS server.
327
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults
328
+ to the current date.
329
+
330
+ Returns: A list of balancing unit prices that match the query.
331
+ """
332
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
333
+ return MarketQuery( # type: ignore[return-value]
334
+ date=date or Date.today(),
335
+ participant=self.participant,
336
+ user=self.user,
337
+ )
@@ -1,10 +1,24 @@
1
1
  """Contains the client layer for making OMI requests to the MMS server."""
2
2
 
3
3
  from logging import getLogger
4
+ from typing import List
5
+ from typing import Optional
4
6
 
7
+ from pydantic_extra_types.pendulum_dt import Date
8
+
9
+ from mms_client.services.base import ClientProto
5
10
  from mms_client.services.base import ServiceConfiguration
11
+ from mms_client.services.base import mms_endpoint
12
+ from mms_client.services.base import mms_multi_endpoint
13
+ from mms_client.types.omi import MarketQuery
14
+ from mms_client.types.omi import MarketSubmit
15
+ from mms_client.types.surplus_capcity import SurplusCapacityData
16
+ from mms_client.types.surplus_capcity import SurplusCapacityQuery
17
+ from mms_client.types.surplus_capcity import SurplusCapacitySubmit
18
+ from mms_client.types.transport import RequestType
6
19
  from mms_client.utils.serialization import SchemaType
7
20
  from mms_client.utils.serialization import Serializer
21
+ from mms_client.utils.web import ClientType
8
22
  from mms_client.utils.web import Interface
9
23
 
10
24
  # Set the default logger for the MMS client
@@ -16,3 +30,88 @@ class OMIClientMixin: # pylint: disable=unused-argument
16
30
 
17
31
  # The configuration for the OMI service
18
32
  config = ServiceConfiguration(Interface.OMI, Serializer(SchemaType.OMI, "MarketData"))
33
+
34
+ @mms_endpoint(
35
+ name="MarketSubmit_RemainingReserveData",
36
+ service=config,
37
+ request_type=RequestType.OMI,
38
+ response_data_type=SurplusCapacityData,
39
+ allowed_clients=[ClientType.BSP],
40
+ )
41
+ def put_surplus_capacity(
42
+ self: ClientProto, request: SurplusCapacitySubmit, date: Optional[Date] = None
43
+ ) -> SurplusCapacityData:
44
+ """Submit an offer to the MMS server.
45
+
46
+ This endpoint is only accessible to BSPs.
47
+
48
+ Arguments:
49
+ request (SurplusCapacitySubmit): The surplus capacity data to submit to the MMS server.
50
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults
51
+ to the current date.
52
+
53
+ Returns: The surplus capacity data that has been registered with the MMS server.
54
+ """
55
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
56
+ return MarketSubmit( # type: ignore[return-value]
57
+ date=date or Date.today(),
58
+ participant=self.participant,
59
+ user=self.user,
60
+ )
61
+
62
+ @mms_multi_endpoint(
63
+ name="MarketSubmit_RemainingReserveData",
64
+ service=config,
65
+ request_type=RequestType.OMI,
66
+ response_data_type=SurplusCapacityData,
67
+ allowed_clients=[ClientType.BSP],
68
+ )
69
+ def put_surplus_capacities(
70
+ self: ClientProto, requests: List[SurplusCapacitySubmit], date: Optional[Date] = None
71
+ ) -> List[SurplusCapacityData]:
72
+ """Submit multiple surplus capacity data to the MMS server.
73
+
74
+ This endpoint is only accessible to BSPs.
75
+
76
+ Arguments:
77
+ requests (list[SurplusCapacitySubmit]): The surplus capacity data to submit to the MMS server.
78
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value
79
+ defaults to the current date.
80
+
81
+ Returns: A list of surplus capacity data that have been registered with the MMS server.
82
+ """
83
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
84
+ return MarketSubmit( # type: ignore[return-value]
85
+ date=date or Date.today(),
86
+ participant=self.participant,
87
+ user=self.user,
88
+ )
89
+
90
+ @mms_multi_endpoint(
91
+ name="MarketQuery_RemainingReserveDataQuery",
92
+ service=config,
93
+ request_type=RequestType.OMI,
94
+ response_envelope_type=MarketSubmit,
95
+ response_data_type=SurplusCapacityData,
96
+ allowed_clients=[ClientType.BSP, ClientType.TSO],
97
+ )
98
+ def query_surplus_capacity(
99
+ self: ClientProto, request: SurplusCapacityQuery, date: Optional[Date] = None
100
+ ) -> List[SurplusCapacityData]:
101
+ """Query the MMS server for surplus capacity data.
102
+
103
+ This endpoint is only accessible to BSPs and TSOs.
104
+
105
+ Arguments:
106
+ request (SurplusCapacityQuery): The query to submit to the MMS server.
107
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to
108
+ the current date.
109
+
110
+ Returns: A list of surplus capacity data that match the query.
111
+ """
112
+ # NOTE: The return type does not match the method definition but the decorator will return the correct type
113
+ return MarketQuery( # type: ignore[return-value]
114
+ date=date or Date.today(),
115
+ participant=self.participant,
116
+ user=self.user,
117
+ )
mms_client/types/award.py CHANGED
@@ -192,6 +192,12 @@ class Award(Payload):
192
192
  # The contract price, in JPY/kW/segment
193
193
  contract_price: Decimal = price("ContractPrice", 10000.00)
194
194
 
195
+ # The unit price charged for the start up cost of the power, in JPY/kW/segment
196
+ start_up_unit_price: Decimal = price("StartUpUnitPrice", 10000.00, True)
197
+
198
+ # The unit price charged for the ramp down cost of the power, in JPY/kW/segment
199
+ ramp_down_unit_price: Decimal = price("RampDownUnitPrice", 10000.00, True)
200
+
195
201
  # The performance evaluation coefficient, alpha
196
202
  performance_evaluation_coefficient: Decimal = attr(name="PerfEvalCoeff", ge=0.00, le=100.0, decimal_places=2)
197
203
 
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[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
 
@@ -0,0 +1,207 @@
1
+ """Contains objects for BUPs."""
2
+
3
+ from decimal import Decimal
4
+ from enum import Enum
5
+ from typing import Annotated
6
+ from typing import List
7
+ from typing import Optional
8
+
9
+ from pendulum import Timezone
10
+ from pydantic import field_serializer
11
+ from pydantic import field_validator
12
+ from pydantic_core import PydanticUndefined
13
+ from pydantic_extra_types.pendulum_dt import DateTime
14
+ from pydantic_xml import attr
15
+ from pydantic_xml import element
16
+ from pydantic_xml import wrapped
17
+
18
+ from mms_client.types.base import Payload
19
+ from mms_client.types.enums import AreaCode
20
+ from mms_client.types.fields import capacity
21
+ from mms_client.types.fields import company_short_name
22
+ from mms_client.types.fields import participant
23
+ from mms_client.types.fields import power_positive
24
+ from mms_client.types.fields import price
25
+ from mms_client.types.fields import resource_name
26
+ from mms_client.types.fields import resource_short_name
27
+ from mms_client.types.fields import system_code
28
+
29
+
30
+ def abc_price(alias: str, optional: bool = False):
31
+ """Create a field for an abc price.
32
+
33
+ Arguments:
34
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
35
+ to the JSON/XML key.
36
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
37
+ required, with no default.
38
+
39
+ Returns: A Pydantic Field object for the abc price.
40
+ """
41
+ return attr(
42
+ default=None if optional else PydanticUndefined, name=alias, gt=-100000.0, lt=100000.0, decimal_places=1
43
+ )
44
+
45
+
46
+ class Status(Enum):
47
+ """Enum representing the possible statuses of a pattern."""
48
+
49
+ INACTIVE = "0"
50
+ ACTIVE = "1"
51
+
52
+
53
+ class StartupCostBand(Payload, tag="BandStartup"):
54
+ """Represents a band of a startup cost."""
55
+
56
+ # The band number which must be unique within the startup cost and will identify this band in sequence.
57
+ number: int = attr(name="CaseNo", ge=1, le=10)
58
+
59
+ # The time at which the startup cost is applied. This value must be greater than or equal to 0.
60
+ stop_time_hours: int = attr(name="StopTime", ge=0, lt=1000)
61
+
62
+ # The V3 unit price charged for this band.
63
+ v3_unit_price: int = attr(name="V3", ge=0, lt=100000000)
64
+
65
+ # The remarks associated with this band.
66
+ remarks: Optional[str] = attr(default=None, name="Remark", min_length=1, max_length=30)
67
+
68
+
69
+ class AbcBand(Payload, tag="BandAbc"):
70
+ """Represents a band of an ABC, whatever that is."""
71
+
72
+ # The band number which must be unique within the ABC and will identify this band in sequence.
73
+ number: int = attr(name="Band", ge=1, le=5)
74
+
75
+ # The capacity from which the band is allowed to operate.
76
+ from_capacity: int = power_positive("FromCap")
77
+
78
+ # The a term of the band.
79
+ a: Decimal = abc_price("a")
80
+
81
+ # The b term of the band.
82
+ b: Decimal = abc_price("b")
83
+
84
+ # The c term of the band.
85
+ c: Decimal = abc_price("c")
86
+
87
+
88
+ class BalancingUnitPriceBand(Payload):
89
+ """Represents a band of a balancing unit price."""
90
+
91
+ # The band number which must be unique within the balancing unit price and will identify this band in sequence.
92
+ number: int = attr(name="Band", ge=1, le=20)
93
+
94
+ # The capacity from which the band is allowed to operate. If the resource_type on the associated resource is set to
95
+ # THERMAL or HYDRO then the value of this field must be greater than or equal to 0. Otherwise, the value of this
96
+ # field is unrestricted.
97
+ from_capacity: int = capacity("FromCap", -10000000)
98
+
99
+ # The V1 unit price charged for this band.
100
+ v1_unit_price: Decimal = price("V1", 10000.00)
101
+
102
+ # The V2 unit price charged for this band. This value is only valid when the contract_type on the associated
103
+ # resource is set to anything other than ONLY_POWER_SUPPLY_1.
104
+ v2_unit_price: Annotated[Decimal, price("V2", 10000.00, True)]
105
+
106
+
107
+ class BalancingUnitPrice(Payload):
108
+ """Represents a balancing unit profile."""
109
+
110
+ # The V4 unit price charged for this pattern. This value is only valid when the contract_type on the associated
111
+ # resource is set to anything other than ONLY_POWER_SUPPLY_1.
112
+ v4_unit_price: Annotated[Decimal, price("V4", 10000.00, True)]
113
+
114
+ # The bands associated with this BUP.
115
+ bands: Annotated[List[BalancingUnitPriceBand], element(tag="BandBup", min_length=1, max_length=20)]
116
+
117
+
118
+ class Pattern(Payload):
119
+ """Represents a pattern associated with a BUP."""
120
+
121
+ # A number identifying this pattern in the overall sequence of patterns
122
+ number: int = attr(name="PatternNo", ge=1, le=10)
123
+
124
+ # The status of the pattern
125
+ status: Status = attr(name="PatternStatus")
126
+
127
+ # Any comments associated with the pattern
128
+ remarks: Optional[str] = attr(default=None, name="PatternRemark", min_length=1, max_length=50)
129
+
130
+ # The balancing unit profile associated with this pattern
131
+ balancing_unit_profile: Optional[BalancingUnitPrice] = element(default=None, tag="Bup")
132
+
133
+ # The quadratic pricing bands associated with this pattern
134
+ abc: Annotated[Optional[List[AbcBand]], wrapped(default=None, path="Abc", min_length=1, max_length=5)]
135
+
136
+ # The startup cost bands associated with this pattern
137
+ startup_costs: Annotated[
138
+ Optional[List[StartupCostBand]], wrapped(default=None, path="StartupCost", min_length=1, max_length=10)
139
+ ]
140
+
141
+
142
+ class BalancingUnitPriceSubmit(Payload, tag="BupSubmit"):
143
+ """Represents the data included with a BUP."""
144
+
145
+ # The resource with which the BUP is associated
146
+ resource_code: str = resource_name("ResourceName")
147
+
148
+ # The start date and time for the validity period of the BUP
149
+ start: DateTime = attr(name="StartTime")
150
+
151
+ # The end date and time for the validity period of the BUP
152
+ end: DateTime = attr(name="EndTime")
153
+
154
+ # The patterns associated with this BUP
155
+ patterns: Annotated[List[Pattern], element(tag="PatternData", max_length=10)]
156
+
157
+ # The name of the BSP participant submitting the BUP. This will only be populated when the object is returned.
158
+ participant_name: Optional[str] = participant("BspParticipantName", True)
159
+
160
+ # The name of the company submitting the BUP. This will only be populated when the object is returned.
161
+ company: Optional[str] = company_short_name("CompanyShortName", True)
162
+
163
+ # The area associated with the BUP. This will only be populated when the object is returned.
164
+ area: Optional[AreaCode] = attr(default=None, name="Area")
165
+
166
+ # The name of the resource being traded. This will only be populated when the object is returned.
167
+ resource_name: Optional[str] = resource_short_name("ResourceShortName", True)
168
+
169
+ # The MMS code of the business entity to which the registration applies. This will only be populated when the
170
+ # object is returned.
171
+ system_code: Optional[str] = system_code("SystemCode", True)
172
+
173
+ @field_serializer("start", "end")
174
+ def encode_datetime(self, value: DateTime) -> str:
175
+ """Encode the datetime to an MMS-compliant ISO 8601 string."""
176
+ return value.replace(tzinfo=None).isoformat() if value else ""
177
+
178
+ @field_validator("start", "end")
179
+ def decode_datetime(cls, value: DateTime) -> DateTime: # pylint: disable=no-self-argument
180
+ """Decode the datetime from an MMS-compliant ISO 8601 string."""
181
+ return value.replace(tzinfo=Timezone("Asia/Tokyo"))
182
+
183
+
184
+ class BalancingUnitPriceQuery(Payload, tag="BupQuery"):
185
+ """Represents the data included with a BUP query."""
186
+
187
+ # Whether or not the BUP is the default
188
+ is_default: Optional[bool] = attr(default=None, name="StandingFlag")
189
+
190
+ # The resource with which the BUP is associated
191
+ resource_code: str = resource_name("ResourceName")
192
+
193
+ # The start date and time for the validity period of the BUP
194
+ start: Annotated[DateTime, attr(default=None, name="StartTime")]
195
+
196
+ # The end date and time for the validity period of the BUP
197
+ end: Annotated[DateTime, attr(default=None, name="EndTime")]
198
+
199
+ @field_serializer("start", "end")
200
+ def encode_datetime(self, value: DateTime) -> str:
201
+ """Encode the datetime to an MMS-compliant ISO 8601 string."""
202
+ return value.replace(tzinfo=None).isoformat() if value else ""
203
+
204
+ @field_validator("start", "end")
205
+ def decode_datetime(cls, value: DateTime) -> DateTime: # pylint: disable=no-self-argument
206
+ """Decode the datetime from an MMS-compliant ISO 8601 string."""
207
+ return value.replace(tzinfo=Timezone("Asia/Tokyo"))