mms-client 1.0.6__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,6 +10,7 @@ from typing import Optional
10
10
  from typing import Protocol
11
11
  from typing import Tuple
12
12
  from typing import Type
13
+ from typing import Union
13
14
 
14
15
  from mms_client.security.crypto import Certificate
15
16
  from mms_client.security.crypto import CryptoWrapper
@@ -119,7 +120,7 @@ class ClientProto(Protocol):
119
120
  def request_many(
120
121
  self,
121
122
  envelope: E,
122
- data: P,
123
+ data: Union[P, List[P]],
123
124
  config: EndpointConfiguration,
124
125
  ) -> Tuple[MultiResponse[E, P], Dict[str, bytes]]:
125
126
  """Submit a request to the MMS server and return the multi-response.
@@ -361,7 +362,7 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
361
362
  def request_many(
362
363
  self,
363
364
  envelope: E,
364
- payload: P,
365
+ payload: Union[P, List[P]],
365
366
  config: EndpointConfiguration[E, P],
366
367
  ) -> Tuple[MultiResponse[E, P], Dict[str, bytes]]:
367
368
  """Submit a request to the MMS server and return the multi-response.
@@ -374,13 +375,20 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
374
375
  Returns: The multi-response from the MMS server.
375
376
  """
376
377
  # First, create the MMS request from the payload and data.
378
+ is_list = isinstance(payload, list)
379
+ data_type = type(payload[0]) if is_list else type(payload) # type: ignore[index]
377
380
  self._logger.debug(
378
381
  (
379
382
  f"{config.name}: Starting multi-request. Envelope: {type(envelope).__name__}, "
380
- f"Data: {type(payload).__name__}"
383
+ f"Data: {data_type.__name__}"
381
384
  ),
382
385
  )
383
- request = self._to_mms_request(config.request_type, config.service.serializer.serialize(envelope, payload))
386
+ serialized = (
387
+ config.service.serializer.serialize_multi(envelope, payload, data_type) # type: ignore[arg-type]
388
+ if is_list
389
+ else config.service.serializer.serialize(envelope, payload) # type: ignore[type-var]
390
+ )
391
+ request = self._to_mms_request(config.request_type, serialized)
384
392
 
385
393
  # Next, submit the request to the MMS server and get and verify the response.
386
394
  resp = self._get_wrapper(config.service).submit(request)
@@ -391,8 +399,12 @@ class BaseClient: # pylint: disable=too-many-instance-attributes
391
399
 
392
400
  # Finally, deserialize and verify the response
393
401
  envelope_type = config.response_envelope_type or type(envelope)
394
- data_type = config.response_data_type or type(payload)
395
- data: MultiResponse[E, P] = config.service.serializer.deserialize_multi(resp.payload, envelope_type, data_type)
402
+ data_type = config.response_data_type or data_type
403
+ data: MultiResponse[E, P] = config.service.serializer.deserialize_multi(
404
+ resp.payload,
405
+ envelope_type,
406
+ data_type, # type: ignore[arg-type]
407
+ )
396
408
  self._verify_multi_response(data, config)
397
409
 
398
410
  # Return the response data and any attachments
@@ -54,6 +54,32 @@ class MarketClientMixin: # pylint: disable=unused-argument
54
54
  days=days,
55
55
  )
56
56
 
57
+ @mms_multi_endpoint("MarketSubmit_OfferData", config, RequestType.INFO, ClientType.BSP)
58
+ def put_offers(
59
+ self: ClientProto, requests: List[OfferData], market_type: MarketType, days: int, date: Optional[Date] = None
60
+ ) -> List[OfferData]:
61
+ """Submit multiple offers to the MMS server.
62
+
63
+ This endpoint is only accessible to BSPs.
64
+
65
+ Arguments:
66
+ requests (List[OfferData]): The offers to submit to the MMS server.
67
+ market_type (MarketType): The type of market for which the offers are being submitted.
68
+ days (int): The number of days ahead for which the offers are being submitted.
69
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to the
70
+ current date.
71
+
72
+ Returns: A list of offers that have been registered with the MMS server.
73
+ """
74
+ # Note: the return type does not match the method definition but the decorator will return the correct type
75
+ return MarketSubmit( # type: ignore[return-value]
76
+ date=date or Date.today(),
77
+ participant=self.participant,
78
+ user=self.user,
79
+ market_type=market_type,
80
+ days=days,
81
+ )
82
+
57
83
  @mms_multi_endpoint(
58
84
  "MarketQuery_OfferQuery", config, RequestType.INFO, resp_envelope_type=MarketSubmit, resp_data_type=OfferData
59
85
  )
@@ -1,8 +1,23 @@
1
1
  """Contains the client layer for making registration requests to the MMS server."""
2
2
 
3
+ from datetime import date as Date
4
+ from typing import List
5
+ from typing import Optional
6
+
7
+ from mms_client.services.base import ClientProto
3
8
  from mms_client.services.base import ServiceConfiguration
9
+ from mms_client.services.base import mms_endpoint
10
+ from mms_client.services.base import mms_multi_endpoint
11
+ from mms_client.types.registration import QueryAction
12
+ from mms_client.types.registration import QueryType
13
+ from mms_client.types.registration import RegistrationQuery
14
+ from mms_client.types.registration import RegistrationSubmit
15
+ from mms_client.types.resource import ResourceData
16
+ from mms_client.types.resource import ResourceQuery
17
+ from mms_client.types.transport import RequestType
4
18
  from mms_client.utils.serialization import SchemaType
5
19
  from mms_client.utils.serialization import Serializer
20
+ from mms_client.utils.web import ClientType
6
21
  from mms_client.utils.web import Interface
7
22
 
8
23
 
@@ -11,3 +26,52 @@ class RegistrationClientMixin: # pylint: disable=unused-argument
11
26
 
12
27
  # The configuration for the registration service
13
28
  config = ServiceConfiguration(Interface.MI, Serializer(SchemaType.REGISTRATION, "RegistrationData"))
29
+
30
+ @mms_endpoint("RegistrationSubmit_Resource", config, RequestType.REGISTRATION, ClientType.BSP)
31
+ def put_resource(self: ClientProto, request: ResourceData) -> ResourceData:
32
+ """Submit a new resource to the MMS server.
33
+
34
+ This endpoint is only accessible to BSPs.
35
+
36
+ Arguments:
37
+ request (ResourceData): The resource to register with the MMS server.
38
+
39
+ Returns: The resource that has been registered with the MMS server.
40
+ """
41
+ # For some reason, the registration DTOs require that the participant ID exist on the payload rather than on
42
+ # the envelope so we need to set it before we return the envelope.
43
+ request.participant = self.participant
44
+
45
+ # Create and return the registration submit DTO.
46
+ return RegistrationSubmit() # type: ignore[return-value]
47
+
48
+ @mms_multi_endpoint(
49
+ "RegistrationQuery_Resource",
50
+ config,
51
+ RequestType.REGISTRATION,
52
+ resp_envelope_type=RegistrationSubmit,
53
+ resp_data_type=ResourceData,
54
+ )
55
+ def query_resources(
56
+ self: ClientProto, request: ResourceQuery, action: QueryAction, date: Optional[Date] = None
57
+ ) -> List[ResourceData]:
58
+ """Query resources from the MMS server.
59
+
60
+ Arguments:
61
+ request (ResourceQuery): The query to send to the MMS server.
62
+ action (QueryAction): The type of query being made. NORMAL for all records or LATEST for the most recent.
63
+ date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to the
64
+ current date.
65
+
66
+ Returns: A list of resources that match the query.
67
+ """
68
+ # For some reason, the registration DTOs require that the participant ID exist on the payload rather than on
69
+ # the envelope so we need to set it before we return the envelope.
70
+ request.participant = self.participant
71
+
72
+ # Inject our parameters into the query and return it.
73
+ return RegistrationQuery( # type: ignore[return-value]
74
+ action=action,
75
+ query_type=QueryType.TRADE,
76
+ date=date or Date.today(),
77
+ )
mms_client/types/base.py CHANGED
@@ -14,6 +14,8 @@ from pydantic_xml import BaseXmlModel
14
14
  from pydantic_xml import attr
15
15
  from pydantic_xml import element
16
16
 
17
+ from mms_client.types.fields import transaction_id
18
+
17
19
 
18
20
  class ValidationStatus(Enum):
19
21
  """Represents the status of the validation check done on an element."""
@@ -67,7 +69,7 @@ class ProcessingStatistics(BaseXmlModel):
67
69
  time_ms: Optional[int] = attr(default=None, name="ProcessingTimeMs")
68
70
 
69
71
  # The transaction ID of the request
70
- transaction_id: Optional[str] = attr(default=None, name="TransactionID", min_length=8, max_length=10)
72
+ transaction_id: Optional[str] = transaction_id("TransactionId", True)
71
73
 
72
74
  # When the request was received, in the format "DDD MMM DD HH:MM:SS TZ YYYY"
73
75
  timestamp: str = attr(default="", name="TimeStamp")
mms_client/types/enums.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Contains enums common to all MMS types."""
2
2
 
3
3
  from enum import Enum
4
+ from enum import IntEnum
4
5
 
5
6
 
6
7
  class AreaCode(Enum):
@@ -16,3 +17,10 @@ class AreaCode(Enum):
16
17
  SHIKOKU = "08"
17
18
  KYUSHU = "09"
18
19
  OKINAWA = "10"
20
+
21
+
22
+ class Frequency(IntEnum):
23
+ """Represents the frequency of power sources."""
24
+
25
+ EAST = 50
26
+ WEST = 60
@@ -4,7 +4,13 @@ from pydantic_core import PydanticUndefined
4
4
  from pydantic_xml import attr
5
5
 
6
6
  # Describes the regular expression required by the MMS API for Japanese text
7
- JAPANESE_TEXT = r"^[\u3000-\u30FF\uFF00-\uFF60\uFFA0-\uFFEF\u4E00-\u9FEA]*$"
7
+ JAPANESE_TEXT = r"[\u3000-\u30FF\uFF00-\uFF60\uFFA0-\uFFEF\u4E00-\u9FEA]*"
8
+
9
+ # Describes the regular expression required by the MMS API for ASCII text
10
+ ASCII_TEXT = r"[a-zA-Z0-9 ~!@#$*()_+}{:?>`='/.,%;\^\|\-\]\[\\<&"]*"
11
+
12
+ # Describes the regular expression required by the MMS API for Japanese or ASCII text
13
+ JAPANESE_ASCII_TEXT = f"{JAPANESE_TEXT}|{ASCII_TEXT}"
8
14
 
9
15
 
10
16
  def participant(alias: str, optional: bool = False):
@@ -43,6 +49,41 @@ def operator_code(alias: str, optional: bool = False):
43
49
  )
44
50
 
45
51
 
52
+ def transaction_id(alias: str, optional: bool = False):
53
+ """Create a field for a transaction ID.
54
+
55
+ Arguments:
56
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
57
+ to the JSON/XML key.
58
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
59
+ required, with no default.
60
+
61
+ Returns: A Pydantic Field object for the transaction ID.
62
+ """
63
+ return attr(
64
+ default=None if optional else PydanticUndefined,
65
+ name=alias,
66
+ min_length=8,
67
+ max_length=10,
68
+ pattern=r"^[a-zA-Z0-9]{8,10}$",
69
+ )
70
+
71
+
72
+ def capacity(alias: str, minimum: int, optional: bool = False):
73
+ """Create a field for a capacity value.
74
+
75
+ Arguments:
76
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
77
+ to the JSON/XML key.
78
+ minimum (int): The minimum value for the capacity field.
79
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
80
+ required, with no default.
81
+
82
+ Returns: A Pydantic Field object for the capacity value.
83
+ """
84
+ return attr(default=None if optional else PydanticUndefined, name=alias, ge=minimum, le=10000000)
85
+
86
+
46
87
  def power_positive(alias: str, optional: bool = False):
47
88
  """Create a field for a positive power value.
48
89
 
@@ -68,7 +109,21 @@ def price(alias: str, optional: bool = False):
68
109
 
69
110
  Returns: A Pydantic Field object for the price value.
70
111
  """
71
- return attr(default=None if optional else PydanticUndefined, name=alias, ge=0.00, le=10000.00, decimal_places=2)
112
+ return attr(default=None if optional else PydanticUndefined, name=alias, ge=0.00, lt=10000.00, decimal_places=2)
113
+
114
+
115
+ def percentage(alias: str, optional: bool = False):
116
+ """Create a field for a percentage value.
117
+
118
+ Arguments:
119
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
120
+ to the JSON/XML key.
121
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
122
+ required, with no default.
123
+
124
+ Returns: A Pydantic Field object for the percentage value.
125
+ """
126
+ return attr(default=None if optional else PydanticUndefined, name=alias, ge=0.0, le=100.0, decimal_places=1)
72
127
 
73
128
 
74
129
  def dr_patter_number(alias: str, optional: bool = False):
@@ -85,6 +140,22 @@ def dr_patter_number(alias: str, optional: bool = False):
85
140
  return attr(default=None if optional else PydanticUndefined, name=alias, ge=1, le=20)
86
141
 
87
142
 
143
+ def pattern_name(alias: str, optional: bool = False):
144
+ """Create a field for a pattern name.
145
+
146
+ Arguments:
147
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
148
+ to the JSON/XML key.
149
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
150
+ required, with no default.
151
+
152
+ Returns: A Pydantic Field object for the pattern name.
153
+ """
154
+ return attr(
155
+ default=None if optional else PydanticUndefined, name=alias, min_length=1, max_length=20, pattern=JAPANESE_TEXT
156
+ )
157
+
158
+
88
159
  def company_short_name(alias: str, optional: bool = False):
89
160
  """Create a field for a company short name.
90
161
 
@@ -101,6 +172,44 @@ def company_short_name(alias: str, optional: bool = False):
101
172
  )
102
173
 
103
174
 
175
+ def address(alias: str, optional: bool = False):
176
+ """Create a field for an address.
177
+
178
+ Arguments:
179
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
180
+ to the JSON/XML key.
181
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
182
+ required, with no default.
183
+
184
+ Returns: A Pydantic Field object for the address.
185
+ """
186
+ return attr(
187
+ default=None if optional else PydanticUndefined, name=alias, min_length=1, max_length=50, pattern=JAPANESE_TEXT
188
+ )
189
+
190
+
191
+ def phone(alias: str, first_part: bool, optional: bool = False):
192
+ """Create a field for a phone number.
193
+
194
+ Arguments:
195
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
196
+ to the JSON/XML key.
197
+ first_part (bool): If True, the field will be the first part of the phone number. If False, the field will be the
198
+ second part of the phone number.
199
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
200
+ required, with no default.
201
+
202
+ Returns: A Pydantic Field object for the phone number.
203
+ """
204
+ return attr(
205
+ default=None if optional else PydanticUndefined,
206
+ name=alias,
207
+ min_length=1,
208
+ max_length=5 if first_part else 4,
209
+ pattern=r"^[0-9]*$",
210
+ )
211
+
212
+
104
213
  def resource_name(alias: str, optional: bool = False):
105
214
  """Create a field for a resource name.
106
215
 
@@ -151,3 +260,42 @@ def system_code(alias: str, optional: bool = False):
151
260
  return attr(
152
261
  default=None if optional else PydanticUndefined, name=alias, min_length=5, max_length=5, pattern=r"^[A-Z0-9]*$"
153
262
  )
263
+
264
+
265
+ def minute(alias: str, optional: bool = False):
266
+ """Create a field for a minute value.
267
+
268
+ Arguments:
269
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
270
+ to the JSON/XML key.
271
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
272
+ required, with no default.
273
+
274
+ Returns: A Pydantic Field object for the minute value.
275
+ """
276
+ return attr(
277
+ default=None if optional else PydanticUndefined,
278
+ name=alias,
279
+ ge=0,
280
+ le=99,
281
+ )
282
+
283
+
284
+ def hour(alias: str, optional: bool = False):
285
+ """Create a field for an hour value.
286
+
287
+ Arguments:
288
+ alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
289
+ to the JSON/XML key.
290
+ optional (bool): If True, the field will be optional with a default of None. If False, the field will be
291
+ required, with no default.
292
+
293
+ Returns: A Pydantic Field object for the hour value.
294
+ """
295
+ return attr(
296
+ default=None if optional else PydanticUndefined,
297
+ name=alias,
298
+ ge=0.0,
299
+ lt=100.0,
300
+ decimal_places=1,
301
+ )
mms_client/types/offer.py CHANGED
@@ -61,13 +61,24 @@ class OfferStack(Payload):
61
61
  mandatory.
62
62
  """
63
63
 
64
- # A number used to identify this PQ pair within the offer
64
+ # A number used to identify this PQ pair within the offer. Ensure that there are no duplicates of the combination
65
+ # of the same resource, pattern number, start date and time, and bid management number in the submitted data. In
66
+ # case of multiple bids in the same time slot and for the same resource, each bid must have a unique number. Enter
67
+ # '1' for a single bid.
65
68
  number: int = attr(name="StackNumber", ge=1, le=20)
66
69
 
67
- # The minimum quantity that must be provided before the offer can be awarded
70
+ # The minimum quantity that must be provided before the offer can be awarded. For resources with dedicated line
71
+ # control or monitoring methods, must be 5000kW or higher. For resources with simple command (online) control
72
+ # or monitoring methods, must be 1000kW or higher.
68
73
  minimum_quantity_kw: int = power_positive("MinimumQuantityInKw")
69
74
 
70
- # The primary bid quantity in kW
75
+ # The primary bid quantity in kW. Must be equal to or greater than minimum_quantity_kw. For non-VPP resources, the
76
+ # total bid volume of all records with the same resource and start date and time must be below the maximum supply
77
+ # capacity for the corresponding product category of the power source. However, in the case of tertiary adjustment
78
+ # power 2, it must be the value obtained by subtracting the total agreed capacity of effective tertiary adjustment
79
+ # power 1. For VPP power sources, the total bid volume of all records with the same power source code and start
80
+ # date and time must be below the maximum supply capacity registered for the corresponding product category in the
81
+ # pattern number.
71
82
  primary_qty_kw: Optional[int] = power_positive("PrimaryOfferQuantityInKw", True)
72
83
 
73
84
  # The first secondary bid quantity in kW
@@ -107,7 +118,8 @@ class OfferData(Payload):
107
118
  # The direction of the offer (buy, sell)
108
119
  direction: Direction = attr(name="Direction")
109
120
 
110
- # The type of market for which the offer is being submitted
121
+ # The type of market for which the offer is being submitted. Must be a valid pattern number for the submission date
122
+ # Required for VPP resources. Ensure there are no duplicate pattern numbers for the same resource and start time.
111
123
  pattern_number: Optional[int] = dr_patter_number("DrPatternNumber", True)
112
124
 
113
125
  # The name of the BSP participant submitting the offer
@@ -0,0 +1,47 @@
1
+ """Contains objects for registrations."""
2
+
3
+ # Have to use this becasue pydantic doesn't like pendulum.Date. I've submitted a PR to address this but it hasn't been
4
+ # merged or released yet.
5
+ from datetime import date as Date
6
+ from enum import Enum
7
+ from typing import Optional
8
+
9
+ from pydantic_xml import attr
10
+
11
+ from mms_client.types.base import Envelope
12
+
13
+
14
+ class QueryAction(Enum):
15
+ """Represents the type of query being made."""
16
+
17
+ NORMAL = "NORMAL"
18
+ LATEST = "LATEST"
19
+
20
+
21
+ class QueryType(Enum):
22
+ """Represents the type of data being queried."""
23
+
24
+ TRADE = "TRADE"
25
+
26
+
27
+ class RegistrationSubmit(Envelope):
28
+ """Represents the base fields for a registration request."""
29
+
30
+
31
+ class RegistrationQuery(Envelope):
32
+ """Represents the base fields for a registration query."""
33
+
34
+ # The query type being made.
35
+ # NORMAL: Retrieve all records that match the specified conditions.
36
+ # LATEST: Retrieve only the latest record that matches the specified conditions.
37
+ action: QueryAction = attr(default=QueryAction.NORMAL, name="Action")
38
+
39
+ # The type of data being queried
40
+ query_type: QueryType = attr(default=QueryType.TRADE, name="DateType")
41
+
42
+ # Date of the transaction in the format "YYYY-MM-DD"
43
+ date: Optional[Date] = attr(default=None, name="Date")
44
+
45
+
46
+ class RegistrationApproval(Envelope):
47
+ """Represents the base fields for a registration approval request."""