mms-client 1.2.0__py3-none-any.whl → 1.3.1__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/market.py +34 -9
- mms_client/services/registration.py +2 -0
- mms_client/types/award.py +310 -0
- mms_client/types/base.py +1 -1
- mms_client/types/enums.py +47 -0
- mms_client/types/fields.py +73 -2
- mms_client/types/market.py +6 -3
- mms_client/types/offer.py +3 -31
- mms_client/types/resource.py +4 -32
- mms_client/types/transport.py +2 -2
- mms_client/utils/serialization.py +87 -23
- {mms_client-1.2.0.dist-info → mms_client-1.3.1.dist-info}/METADATA +4 -3
- {mms_client-1.2.0.dist-info → mms_client-1.3.1.dist-info}/RECORD +15 -14
- {mms_client-1.2.0.dist-info → mms_client-1.3.1.dist-info}/LICENSE +0 -0
- {mms_client-1.2.0.dist-info → mms_client-1.3.1.dist-info}/WHEEL +0 -0
mms_client/services/market.py
CHANGED
|
@@ -8,6 +8,8 @@ from mms_client.services.base import ClientProto
|
|
|
8
8
|
from mms_client.services.base import ServiceConfiguration
|
|
9
9
|
from mms_client.services.base import mms_endpoint
|
|
10
10
|
from mms_client.services.base import mms_multi_endpoint
|
|
11
|
+
from mms_client.types.award import AwardQuery
|
|
12
|
+
from mms_client.types.award import AwardResponse
|
|
11
13
|
from mms_client.types.market import MarketCancel
|
|
12
14
|
from mms_client.types.market import MarketQuery
|
|
13
15
|
from mms_client.types.market import MarketSubmit
|
|
@@ -45,7 +47,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
45
47
|
|
|
46
48
|
Returns: The offer that has been registered with the MMS server.
|
|
47
49
|
"""
|
|
48
|
-
#
|
|
50
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
49
51
|
return MarketSubmit( # type: ignore[return-value]
|
|
50
52
|
date=date or Date.today(),
|
|
51
53
|
participant=self.participant,
|
|
@@ -71,7 +73,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
71
73
|
|
|
72
74
|
Returns: A list of offers that have been registered with the MMS server.
|
|
73
75
|
"""
|
|
74
|
-
#
|
|
76
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
75
77
|
return MarketSubmit( # type: ignore[return-value]
|
|
76
78
|
date=date or Date.today(),
|
|
77
79
|
participant=self.participant,
|
|
@@ -83,28 +85,24 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
83
85
|
@mms_multi_endpoint(
|
|
84
86
|
"MarketQuery_OfferQuery", config, RequestType.INFO, resp_envelope_type=MarketSubmit, resp_data_type=OfferData
|
|
85
87
|
)
|
|
86
|
-
def query_offers(
|
|
87
|
-
self: ClientProto, request: OfferQuery, market_type: MarketType, days: int, date: Optional[Date] = None
|
|
88
|
-
) -> List[OfferData]:
|
|
88
|
+
def query_offers(self: ClientProto, request: OfferQuery, days: int, date: Optional[Date] = None) -> List[OfferData]:
|
|
89
89
|
"""Query the MMS server for offers.
|
|
90
90
|
|
|
91
91
|
This endpoint is accessible to all client types.
|
|
92
92
|
|
|
93
93
|
Arguments:
|
|
94
94
|
request (OfferQuery): The query to submit to the MMS server.
|
|
95
|
-
market_type (MarketType): The type of market for which the offer was submitted.
|
|
96
95
|
days (int): The number of days ahead for which the data is being queried.
|
|
97
96
|
date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to the
|
|
98
97
|
current date.
|
|
99
98
|
|
|
100
99
|
Returns: A list of offers that match the query.
|
|
101
100
|
"""
|
|
102
|
-
#
|
|
101
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
103
102
|
return MarketQuery( # type: ignore[return-value]
|
|
104
103
|
date=date or Date.today(),
|
|
105
104
|
participant=self.participant,
|
|
106
105
|
user=self.user,
|
|
107
|
-
market_type=market_type,
|
|
108
106
|
days=days,
|
|
109
107
|
)
|
|
110
108
|
|
|
@@ -123,7 +121,7 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
123
121
|
date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to the
|
|
124
122
|
current date.
|
|
125
123
|
"""
|
|
126
|
-
#
|
|
124
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
127
125
|
return MarketCancel( # type: ignore[return-value]
|
|
128
126
|
date=date or Date.today(),
|
|
129
127
|
participant=self.participant,
|
|
@@ -131,3 +129,30 @@ class MarketClientMixin: # pylint: disable=unused-argument
|
|
|
131
129
|
market_type=market_type,
|
|
132
130
|
days=days,
|
|
133
131
|
)
|
|
132
|
+
|
|
133
|
+
@mms_endpoint("MarketQuery_AwardResultsQuery", config, RequestType.INFO, resp_data_type=AwardResponse)
|
|
134
|
+
def query_awards(self: ClientProto, request: AwardQuery, days: int, date: Optional[Date] = None) -> AwardResponse:
|
|
135
|
+
"""Query the MMS server for award results.
|
|
136
|
+
|
|
137
|
+
This endpoint is accessible to all client types.
|
|
138
|
+
|
|
139
|
+
If no values are specified for Area, Associated Area, Power Generation Unit Code, or GC Registration Flag,
|
|
140
|
+
the results for all areas will be retrieved. If one or more of these criteria are specified, the results will
|
|
141
|
+
be filtered according to the specified criteria. If no value is specified for the retrieval period, the default
|
|
142
|
+
value for this field is 1.
|
|
143
|
+
|
|
144
|
+
Arguments:
|
|
145
|
+
request (AwardQuery): The query to submit to the MMS server.
|
|
146
|
+
days (int): The number of days ahead for which the data is being queried.
|
|
147
|
+
date (Date): The date of the transaction in the format "YYYY-MM-DD". This value defaults to the
|
|
148
|
+
current date.
|
|
149
|
+
|
|
150
|
+
Returns: The award results that match the query.
|
|
151
|
+
"""
|
|
152
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
153
|
+
return MarketQuery( # type: ignore[return-value]
|
|
154
|
+
date=date or Date.today(),
|
|
155
|
+
participant=self.participant,
|
|
156
|
+
user=self.user,
|
|
157
|
+
days=days,
|
|
158
|
+
)
|
|
@@ -38,6 +38,7 @@ class RegistrationClientMixin: # pylint: disable=unused-argument
|
|
|
38
38
|
|
|
39
39
|
Returns: The resource that has been registered with the MMS server.
|
|
40
40
|
"""
|
|
41
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
41
42
|
# For some reason, the registration DTOs require that the participant ID exist on the payload rather than on
|
|
42
43
|
# the envelope so we need to set it before we return the envelope.
|
|
43
44
|
request.participant = self.participant
|
|
@@ -65,6 +66,7 @@ class RegistrationClientMixin: # pylint: disable=unused-argument
|
|
|
65
66
|
|
|
66
67
|
Returns: A list of resources that match the query.
|
|
67
68
|
"""
|
|
69
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
68
70
|
# For some reason, the registration DTOs require that the participant ID exist on the payload rather than on
|
|
69
71
|
# the envelope so we need to set it before we return the envelope.
|
|
70
72
|
request.participant = self.participant
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Contains objects for MMS award results."""
|
|
2
|
+
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from pydantic_core import PydanticUndefined
|
|
9
|
+
from pydantic_extra_types.pendulum_dt import DateTime
|
|
10
|
+
from pydantic_xml import attr
|
|
11
|
+
from pydantic_xml import element
|
|
12
|
+
from pydantic_xml import wrapped
|
|
13
|
+
|
|
14
|
+
from mms_client.types.base import Payload
|
|
15
|
+
from mms_client.types.enums import AreaCode
|
|
16
|
+
from mms_client.types.enums import BooleanFlag
|
|
17
|
+
from mms_client.types.enums import CommandMonitorMethod
|
|
18
|
+
from mms_client.types.enums import ContractResult
|
|
19
|
+
from mms_client.types.enums import Direction
|
|
20
|
+
from mms_client.types.enums import ResourceType
|
|
21
|
+
from mms_client.types.fields import company_short_name
|
|
22
|
+
from mms_client.types.fields import contract_id
|
|
23
|
+
from mms_client.types.fields import dr_patter_number
|
|
24
|
+
from mms_client.types.fields import dr_pattern_name
|
|
25
|
+
from mms_client.types.fields import jbms_id
|
|
26
|
+
from mms_client.types.fields import offer_id
|
|
27
|
+
from mms_client.types.fields import operator_code
|
|
28
|
+
from mms_client.types.fields import participant
|
|
29
|
+
from mms_client.types.fields import power_positive
|
|
30
|
+
from mms_client.types.fields import price
|
|
31
|
+
from mms_client.types.fields import resource_name
|
|
32
|
+
from mms_client.types.fields import resource_short_name
|
|
33
|
+
from mms_client.types.fields import system_code
|
|
34
|
+
from mms_client.types.market import MarketType
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def baseload_file_name(alias: str, optional: bool = False):
|
|
38
|
+
"""Create a field for a baseload file name.
|
|
39
|
+
|
|
40
|
+
Arguments:
|
|
41
|
+
alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
|
|
42
|
+
to the JSON/XML key.
|
|
43
|
+
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
44
|
+
required, with no default.
|
|
45
|
+
|
|
46
|
+
Returns: A Pydantic Field object for the baseload file name.
|
|
47
|
+
"""
|
|
48
|
+
return attr(
|
|
49
|
+
default=None if optional else PydanticUndefined,
|
|
50
|
+
name=alias,
|
|
51
|
+
min_length=31,
|
|
52
|
+
max_length=40,
|
|
53
|
+
pattern=r"^W9_[0-9]{4}_[0-9]{8}_[0-9]{2}_[A-Z0-9]{5}_[A-Z0-9_\-]{1,10}\.xml$",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class SubRequirement(Enum):
|
|
58
|
+
"""Commodity combination categories."""
|
|
59
|
+
|
|
60
|
+
PRIMARY = "PRI"
|
|
61
|
+
SECONDARY_1 = "SEC1"
|
|
62
|
+
SECONDARY_2 = "SEC2"
|
|
63
|
+
TERTIARY_1 = "TER1"
|
|
64
|
+
PRIMARY_SECONDARY_1 = "PRI-SEC1"
|
|
65
|
+
PRIMARY_SECONDARY_2 = "PRI-SEC2"
|
|
66
|
+
PRIMARY_TERTIARY_1 = "PRI-TER1"
|
|
67
|
+
SECONDARY = "SEC1-SEC2"
|
|
68
|
+
SECONDARY_1_TERTIARY_1 = "SEC1-TER1"
|
|
69
|
+
SECONDARY_2_TERTIARY_1 = "SEC2-TER1"
|
|
70
|
+
PRIAMRY_SECONDARY = "PRI-SEC1-SEC2"
|
|
71
|
+
PRIMARY_SECONDARY_1_TERTIARY_1 = "PRI-SEC1-TER1"
|
|
72
|
+
PRIMARY_SECONDARY_2_TERTIARY_1 = "PRI-SEC2-TER1"
|
|
73
|
+
SECONDARY_TERTIARY_1 = "SEC1-SEC2-TER1"
|
|
74
|
+
PRIMARY_SECONDARY_TERTIARY_1 = "PRI-SEC1-SEC2-TER1"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ContractSource(Enum):
|
|
78
|
+
"""Describes the source of the contract."""
|
|
79
|
+
|
|
80
|
+
MA = "1"
|
|
81
|
+
SWITCHING = "2"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class AwardQuery(Payload, tag="AwardResultsQuery"):
|
|
85
|
+
"""Query object for bid awards."""
|
|
86
|
+
|
|
87
|
+
# The market for which results should be retrieved
|
|
88
|
+
market_type: MarketType = attr(name="MarketType")
|
|
89
|
+
|
|
90
|
+
# The area for which results should be retrieved. If this isn't provided, then all results will be retrieved
|
|
91
|
+
area: Optional[AreaCode] = attr(default=None, name="Area")
|
|
92
|
+
|
|
93
|
+
# The associated area. If the API user is a BSP, this field cannot be set
|
|
94
|
+
linked_area: Optional[AreaCode] = attr(default=None, name="LinkedArea")
|
|
95
|
+
|
|
96
|
+
# The name of the resource for which results should be retrieved. If this isn't provided, then all results will be
|
|
97
|
+
# retrieved for all resources
|
|
98
|
+
resource: Optional[str] = resource_name("ResourceName", True)
|
|
99
|
+
|
|
100
|
+
# The start date and time for which results should be retrieved. Should conform with the start of a block
|
|
101
|
+
start: DateTime = attr(name="StartTime")
|
|
102
|
+
|
|
103
|
+
# The end date and time for which results should be retrieved. Should conform with the end of a block. You can
|
|
104
|
+
# specify the end date and time of any block within the period from the transaction date to the number of days to
|
|
105
|
+
# get multiple blocks.
|
|
106
|
+
end: DateTime = attr(name="EndTime")
|
|
107
|
+
|
|
108
|
+
# Whether we are before gate close or after gate close. If this isn't provided, then all results will be retrieved
|
|
109
|
+
# regardless of gate closure.
|
|
110
|
+
gate_closed: Optional[BooleanFlag] = attr(default=None, name="AfterGC")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Award(Payload):
|
|
114
|
+
"""Represents the details of a bid award."""
|
|
115
|
+
|
|
116
|
+
# Unique identifier assigned to the contract
|
|
117
|
+
contract_id: str = contract_id("ContractId")
|
|
118
|
+
|
|
119
|
+
# Unique identifier assigned to the contract by the JBMS
|
|
120
|
+
jbms_id: int = jbms_id("JbmsId")
|
|
121
|
+
|
|
122
|
+
# The area of the power generation unit generating the electricity
|
|
123
|
+
area: AreaCode = attr(name="Area")
|
|
124
|
+
|
|
125
|
+
# The associated area. If the API user is a BSP, it is not set.
|
|
126
|
+
linked_area: Optional[AreaCode] = attr(default=None, name="LinkedArea")
|
|
127
|
+
|
|
128
|
+
# A code identifying the power generation unit for which this bid was awarded
|
|
129
|
+
resource: str = resource_name("ResourceName")
|
|
130
|
+
|
|
131
|
+
# An abbreviated name for the power generation unit for which this bid was awarded
|
|
132
|
+
resource_short_name: str = resource_short_name("ResourceShortName")
|
|
133
|
+
|
|
134
|
+
# The grid code of the resource being traded
|
|
135
|
+
system_code: str = system_code("SystemCode")
|
|
136
|
+
|
|
137
|
+
# How the power generation unit produces electricity
|
|
138
|
+
resource_type: ResourceType = attr(name="ResourceType")
|
|
139
|
+
|
|
140
|
+
# The type of market for which the offer is being submitted. Must be a valid pattern number for the submission date
|
|
141
|
+
# Required for VPP resources. Ensure there are no duplicate pattern numbers for the same resource and start time.
|
|
142
|
+
pattern_number: Optional[int] = dr_patter_number("DrPatternNumber", True)
|
|
143
|
+
|
|
144
|
+
# The name of the list pattern with which this award is associated
|
|
145
|
+
pattern_name: Optional[str] = dr_pattern_name("DrPatternName", True)
|
|
146
|
+
|
|
147
|
+
# The participant ID of the BSP associated with this bid award
|
|
148
|
+
bsp_participant: str = participant("BspParticipantName")
|
|
149
|
+
|
|
150
|
+
# The abbreviated name of the counterparty
|
|
151
|
+
company_short_name: str = company_short_name("CompanyShortName")
|
|
152
|
+
|
|
153
|
+
# A code identifying the TSO or MO
|
|
154
|
+
operator: str = operator_code("OperatorCode")
|
|
155
|
+
|
|
156
|
+
# Primary-Secondary 1 command-and-control and monitoring method. Refers to the methodology or system utilized for
|
|
157
|
+
# regulating and overseeing power generation units' operations in response to external commands or signals. This
|
|
158
|
+
# method encompasses the process by which instructions are transmitted to power generation units, and their
|
|
159
|
+
# performance is monitored to ensure compliance with these instructions. If has_secondary_1 is set to True, then
|
|
160
|
+
# it must be set to DEDICATED_LINE. At least one of the following combinations must be set:
|
|
161
|
+
# primary_secondary_1_control_method
|
|
162
|
+
# secondary_2_tertiary_control_method
|
|
163
|
+
primary_secondary_1_control_method: Optional[CommandMonitorMethod] = attr(
|
|
164
|
+
default=None, name="CommandOperationMethodPriSec1"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Secondary 2-Tertiary command-and-control and monitoring method. Refers to the methodology or system utilized for
|
|
168
|
+
# regulating and overseeing power generation units' operations in response to external commands or signals. This
|
|
169
|
+
# method encompasses the process by which instructions are transmitted to power generation units, and their
|
|
170
|
+
# performance is monitored to ensure compliance with these instructions.
|
|
171
|
+
secondary_2_tertiary_control_method: Optional[CommandMonitorMethod] = attr(
|
|
172
|
+
default=None, name="CommandOperationMethodSec2Ter1Ter2"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Bid unit price, in JPY/kW/segment
|
|
176
|
+
offer_price: Decimal = price("OfferPrice", 10000.00)
|
|
177
|
+
|
|
178
|
+
# The contract price, in JPY/kW/segment
|
|
179
|
+
contract_price: Decimal = price("ContractPrice", 10000.00)
|
|
180
|
+
|
|
181
|
+
# The performance evaluation coefficient, alpha
|
|
182
|
+
performance_evaluation_coefficient: Decimal = attr(name="PerfEvalCoeff", ge=0.00, le=100.0, decimal_places=2)
|
|
183
|
+
|
|
184
|
+
# The corrected unit price, in JPY/kW/segment
|
|
185
|
+
corrected_unit_price: Decimal = price("CorrectedUnitPrice", 1000000.00)
|
|
186
|
+
|
|
187
|
+
# The commodity combination category. This is only set for non-BSP users.
|
|
188
|
+
sub_requirement: Optional[SubRequirement] = attr(default=None, name="SubRequirementType")
|
|
189
|
+
|
|
190
|
+
# The primary offer quantity in kW. If the API user is a BSP, this field is not set for the contract results
|
|
191
|
+
# generated by the TSO's power source exchange.
|
|
192
|
+
primary_offer_qty: Optional[int] = power_positive("PrimaryOfferQuantityInKw", True)
|
|
193
|
+
|
|
194
|
+
# The secondary 1 offer quantity, in kW. If the API user is a BSP, this field is not set for the contract results
|
|
195
|
+
# generated by the TSO's power source exchange.
|
|
196
|
+
secondary_1_offer_qty: Optional[int] = power_positive("Secondary1OfferQuantityInKw", True)
|
|
197
|
+
|
|
198
|
+
# The secondary 2 offer quantity, in kW. If the API user is a BSP, this field is not set for the contract results
|
|
199
|
+
# generated by the TSO's power source exchange.
|
|
200
|
+
secondary_2_offer_qty: Optional[int] = power_positive("Secondary2OfferQuantityInKw", True)
|
|
201
|
+
|
|
202
|
+
# The tertiary 1 offer quantity, in kW. If the API user is a BSP, this field is not set for the contract results
|
|
203
|
+
# generated by the TSO's power source exchange.
|
|
204
|
+
tertiary_1_offer_qty: Optional[int] = power_positive("Tertiary1OfferQuantityInKw", True)
|
|
205
|
+
|
|
206
|
+
# The tertiary 2 offer quantity, in kW. If the API user is a BSP, this field is not set for the contract results
|
|
207
|
+
# generated by the TSO's power source exchange.
|
|
208
|
+
tertiary_2_offer_qty: Optional[int] = power_positive("Tertiary2OfferQuantityInKw", True)
|
|
209
|
+
|
|
210
|
+
# The primary award quantity, in kW
|
|
211
|
+
primary_award_qty: Optional[int] = power_positive("PrimaryAwardQuantityInKw", True)
|
|
212
|
+
|
|
213
|
+
# The secondary 1 award quantity, in kW
|
|
214
|
+
secondary_1_award_qty: Optional[int] = power_positive("Secondary1AwardQuantityInKw", True)
|
|
215
|
+
|
|
216
|
+
# The secondary 2 award quantity, in kW
|
|
217
|
+
secondary_2_award_qty: Optional[int] = power_positive("Secondary2AwardQuantityInKw", True)
|
|
218
|
+
|
|
219
|
+
# The tertiary 1 award quantity, in kW
|
|
220
|
+
tertiary_1_award_qty: Optional[int] = power_positive("Tertiary1AwardQuantityInKw", True)
|
|
221
|
+
|
|
222
|
+
# The tertiary 2 award quantity, in kW
|
|
223
|
+
tertiary_2_award_qty: Optional[int] = power_positive("Tertiary2AwardQuantityInKw", True)
|
|
224
|
+
|
|
225
|
+
# The primary contract quantity, in kW
|
|
226
|
+
primary_contract_qty: Optional[int] = power_positive("PrimaryContractQuantityInKw", True)
|
|
227
|
+
|
|
228
|
+
# The secondary 1 contract quantity, in kW
|
|
229
|
+
secondary_1_contract_qty: Optional[int] = power_positive("Secondary1ContractQuantityInKw", True)
|
|
230
|
+
|
|
231
|
+
# The secondary 2 contract quantity, in kW
|
|
232
|
+
secondary_2_contract_qty: Optional[int] = power_positive("Secondary2ContractQuantityInKw", True)
|
|
233
|
+
|
|
234
|
+
# The tertiary 1 contract quantity, in kW
|
|
235
|
+
tertiary_1_contract_qty: Optional[int] = power_positive("Tertiary1ContractQuantityInKw", True)
|
|
236
|
+
|
|
237
|
+
# The tertiary 2 contract quantity, in kW
|
|
238
|
+
tertiary_2_contract_qty: Optional[int] = power_positive("Tertiary2ContractQuantityInKw", True)
|
|
239
|
+
|
|
240
|
+
# The primary effective contracted quantity, in kW
|
|
241
|
+
primary_valid_qty: Optional[int] = power_positive("PrimaryValidQuantityInKw", True)
|
|
242
|
+
|
|
243
|
+
# The secondary 1 effective contracted quantity, in kW
|
|
244
|
+
secondary_1_valid_qty: Optional[int] = power_positive("Secondary1ValidQuantityInKw", True)
|
|
245
|
+
|
|
246
|
+
# The secondary 2 effective contracted quantity, in kW
|
|
247
|
+
secondary_2_valid_qty: Optional[int] = power_positive("Secondary2ValidQuantityInKw", True)
|
|
248
|
+
|
|
249
|
+
# The tertiary 1 effective contracted quantity, in kW
|
|
250
|
+
tertiary_1_valid_qty: Optional[int] = power_positive("Tertiary1ValidQuantityInKw", True)
|
|
251
|
+
|
|
252
|
+
# The compound fulfillment quantity, in kW
|
|
253
|
+
compound_valid_qty: Optional[int] = power_positive("CompoundValidQuantityInKw", True)
|
|
254
|
+
|
|
255
|
+
# The primary invalid contract quantity, in kW
|
|
256
|
+
primary_invalid_qty: Optional[int] = power_positive("PrimaryInvalidQuantityInKw", True)
|
|
257
|
+
|
|
258
|
+
# The secondary 1 invalid contract quantity, in kW
|
|
259
|
+
secondary_1_invalid_qty: Optional[int] = power_positive("Secondary1InvalidQuantityInKw", True)
|
|
260
|
+
|
|
261
|
+
# The secondary 2 invalid contract quantity, in kW
|
|
262
|
+
secondary_2_invalid_qty: Optional[int] = power_positive("Secondary2InvalidQuantityInKw", True)
|
|
263
|
+
|
|
264
|
+
# The tertiary 1 invalid contract quantity, in kW
|
|
265
|
+
tertiary_1_invalid_qty: Optional[int] = power_positive("Tertiary1InvalidQuantityInKw", True)
|
|
266
|
+
|
|
267
|
+
# Name of the file containing the negative baseload data
|
|
268
|
+
negative_baseload_file: Optional[str] = baseload_file_name("BaselineLoadFileNameNeg", True)
|
|
269
|
+
|
|
270
|
+
# Name of the file containing the positive baseload data
|
|
271
|
+
positive_baseload_file: Optional[str] = baseload_file_name("BaselineLoadFileNamePos", True)
|
|
272
|
+
|
|
273
|
+
# The date and time when the offer was submitted. If the API user is a BSP, this attribute is not set for the
|
|
274
|
+
# contract results generated by the TSO's power source exchange.
|
|
275
|
+
submission_time: Optional[DateTime] = attr(default=None, name="SubmissionTime")
|
|
276
|
+
|
|
277
|
+
# Contract result (full, partial)
|
|
278
|
+
offer_award_level: ContractResult = attr(name="OfferAwardedLevel")
|
|
279
|
+
|
|
280
|
+
# The ID of the offer to which this stack belongs
|
|
281
|
+
offer_id: Optional[str] = offer_id("OfferId", True)
|
|
282
|
+
|
|
283
|
+
# The source of the contract
|
|
284
|
+
contract_source: ContractSource = attr(name="ContractSource")
|
|
285
|
+
|
|
286
|
+
# Whether we are before gate close or after gate close
|
|
287
|
+
gate_closed: BooleanFlag = attr(name="AfterGC")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class AwardResult(Payload, tag="AwardResults"):
|
|
291
|
+
"""Contains a number of bid rewards associated with a block of time and trade direction."""
|
|
292
|
+
|
|
293
|
+
# The start date and time of the block associated with the awards
|
|
294
|
+
start: DateTime = attr(name="StartTime")
|
|
295
|
+
|
|
296
|
+
# The end date and time of the block associated with the awards
|
|
297
|
+
end: DateTime = attr(name="EndTime")
|
|
298
|
+
|
|
299
|
+
# The direction of the associated trades
|
|
300
|
+
direction: Direction = attr(name="Direction")
|
|
301
|
+
|
|
302
|
+
# The bid awards associated with these parameters
|
|
303
|
+
data: List[Award] = element(tag="AwardResultsData", min_length=1)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class AwardResponse(AwardQuery, tag="AwardResultsQuery"):
|
|
307
|
+
"""Contains the results of a bid award query."""
|
|
308
|
+
|
|
309
|
+
# The bid awards associated with the query
|
|
310
|
+
results: Optional[List[AwardResult]] = wrapped(default=None, path="AwardResultsQueryResponse")
|
mms_client/types/base.py
CHANGED
|
@@ -115,7 +115,7 @@ class SchemaType(Enum):
|
|
|
115
115
|
OMI = "omi.xsd"
|
|
116
116
|
|
|
117
117
|
|
|
118
|
-
class PayloadBase(BaseXmlModel, nsmap={"xsi": "http://www.w3.org/2001/XMLSchema"}):
|
|
118
|
+
class PayloadBase(BaseXmlModel, nsmap={"xsi": "http://www.w3.org/2001/XMLSchema-instance"}):
|
|
119
119
|
"""Represents the base fields for an MMS request payload."""
|
|
120
120
|
|
|
121
121
|
# The XML schema to use for validation
|
mms_client/types/enums.py
CHANGED
|
@@ -19,8 +19,55 @@ class AreaCode(Enum):
|
|
|
19
19
|
OKINAWA = "10"
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
class Direction(Enum):
|
|
23
|
+
"""Represents the reserve direction of the offer."""
|
|
24
|
+
|
|
25
|
+
SELL = "1" # Increasing the reserves (sell)
|
|
26
|
+
# Note: Support for the BUY direction was removed from the MMS API
|
|
27
|
+
# BUY = "2" # Decreasing the reserves (buy)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ContractResult(Enum):
|
|
31
|
+
"""Represents the result of a contract."""
|
|
32
|
+
|
|
33
|
+
FULL = "1"
|
|
34
|
+
PARTIAL = "2"
|
|
35
|
+
|
|
36
|
+
|
|
22
37
|
class Frequency(IntEnum):
|
|
23
38
|
"""Represents the frequency of power sources."""
|
|
24
39
|
|
|
25
40
|
EAST = 50
|
|
26
41
|
WEST = 60
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ResourceType(Enum):
|
|
45
|
+
"""How the power generation unit produces electricity."""
|
|
46
|
+
|
|
47
|
+
THERMAL = "01"
|
|
48
|
+
HYDRO = "02"
|
|
49
|
+
PUMP = "03"
|
|
50
|
+
BATTERY = "04"
|
|
51
|
+
VPP_GEN = "05"
|
|
52
|
+
VPP_GEN_AND_DEM = "06"
|
|
53
|
+
VPP_DEM = "07"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CommandMonitorMethod(Enum):
|
|
57
|
+
"""Describes how the power generation unit is monitored and commanded."""
|
|
58
|
+
|
|
59
|
+
DEDICATED_LINE = "1"
|
|
60
|
+
SIMPLE_COMMAND = "2"
|
|
61
|
+
OFFLINE = "3"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class BooleanFlag(Enum):
|
|
65
|
+
"""Represents a Boolean value as an enumeration.
|
|
66
|
+
|
|
67
|
+
There are many places throughout the MMS documenation where Boolean values are treated as enums for reasons that are
|
|
68
|
+
not clear. This is a common pattern and this class is provided to make it easier to handle these cases without
|
|
69
|
+
having many different enum classes
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
YES = "1"
|
|
73
|
+
NO = "0"
|
mms_client/types/fields.py
CHANGED
|
@@ -69,6 +69,26 @@ def transaction_id(alias: str, optional: bool = False):
|
|
|
69
69
|
)
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
def offer_id(alias: str, optional: bool = False):
|
|
73
|
+
"""Create a field for an offer ID.
|
|
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
|
+
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
79
|
+
required, with no default.
|
|
80
|
+
|
|
81
|
+
Returns: A Pydantic Field object for the offer ID.
|
|
82
|
+
"""
|
|
83
|
+
return attr(
|
|
84
|
+
default=None if optional else PydanticUndefined,
|
|
85
|
+
name=alias,
|
|
86
|
+
min_length=1,
|
|
87
|
+
max_length=30,
|
|
88
|
+
pattern=r"^[a-zA-Z0-9_-]*$",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
72
92
|
def capacity(alias: str, minimum: int, optional: bool = False):
|
|
73
93
|
"""Create a field for a capacity value.
|
|
74
94
|
|
|
@@ -98,18 +118,19 @@ def power_positive(alias: str, optional: bool = False):
|
|
|
98
118
|
return attr(default=None if optional else PydanticUndefined, name=alias, gt=0, le=10000000)
|
|
99
119
|
|
|
100
120
|
|
|
101
|
-
def price(alias: str, optional: bool = False):
|
|
121
|
+
def price(alias: str, limit: float, optional: bool = False):
|
|
102
122
|
"""Create a field for a price value.
|
|
103
123
|
|
|
104
124
|
Arguments:
|
|
105
125
|
alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
|
|
106
126
|
to the JSON/XML key.
|
|
127
|
+
limit (int): The maximum value for the price field.
|
|
107
128
|
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
108
129
|
required, with no default.
|
|
109
130
|
|
|
110
131
|
Returns: A Pydantic Field object for the price value.
|
|
111
132
|
"""
|
|
112
|
-
return attr(default=None if optional else PydanticUndefined, name=alias, ge=0.00,
|
|
133
|
+
return attr(default=None if optional else PydanticUndefined, name=alias, ge=0.00, le=limit, decimal_places=2)
|
|
113
134
|
|
|
114
135
|
|
|
115
136
|
def percentage(alias: str, optional: bool = False):
|
|
@@ -140,6 +161,22 @@ def dr_patter_number(alias: str, optional: bool = False):
|
|
|
140
161
|
return attr(default=None if optional else PydanticUndefined, name=alias, ge=1, le=20)
|
|
141
162
|
|
|
142
163
|
|
|
164
|
+
def dr_pattern_name(alias: str, optional: bool = False):
|
|
165
|
+
"""Create a field for a DR pattern name.
|
|
166
|
+
|
|
167
|
+
Arguments:
|
|
168
|
+
alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
|
|
169
|
+
to the JSON/XML key.
|
|
170
|
+
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
171
|
+
required, with no default.
|
|
172
|
+
|
|
173
|
+
Returns: A Pydantic Field object for the DR pattern name.
|
|
174
|
+
"""
|
|
175
|
+
return attr(
|
|
176
|
+
default=None if optional else PydanticUndefined, name=alias, min_length=1, max_length=20, pattern=JAPANESE_TEXT
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
143
180
|
def pattern_name(alias: str, optional: bool = False):
|
|
144
181
|
"""Create a field for a pattern name.
|
|
145
182
|
|
|
@@ -246,6 +283,40 @@ def resource_short_name(alias: str, optional: bool = False):
|
|
|
246
283
|
)
|
|
247
284
|
|
|
248
285
|
|
|
286
|
+
def contract_id(alias: str, optional: bool = False):
|
|
287
|
+
"""Create a field for a contract ID.
|
|
288
|
+
|
|
289
|
+
Arguments:
|
|
290
|
+
alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
|
|
291
|
+
to the JSON/XML key.
|
|
292
|
+
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
293
|
+
required, with no default.
|
|
294
|
+
|
|
295
|
+
Returns: A Pydantic Field object for the contract ID.
|
|
296
|
+
"""
|
|
297
|
+
return attr(
|
|
298
|
+
default=None if optional else PydanticUndefined,
|
|
299
|
+
name=alias,
|
|
300
|
+
min_length=19,
|
|
301
|
+
max_length=19,
|
|
302
|
+
pattern=r"^[a-zA-Z0-9]**$",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def jbms_id(alias: str, optional: bool = False):
|
|
307
|
+
"""Create a field for a JBMS ID.
|
|
308
|
+
|
|
309
|
+
Arguments:
|
|
310
|
+
alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
|
|
311
|
+
to the JSON/XML key.
|
|
312
|
+
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
313
|
+
required, with no default.
|
|
314
|
+
|
|
315
|
+
Returns: A Pydantic Field object for the JBMS ID.
|
|
316
|
+
"""
|
|
317
|
+
return attr(default=None if optional else PydanticUndefined, name=alias, ge=1, lt=1000000000000000000)
|
|
318
|
+
|
|
319
|
+
|
|
249
320
|
def system_code(alias: str, optional: bool = False):
|
|
250
321
|
"""Create a field for a system code.
|
|
251
322
|
|
mms_client/types/market.py
CHANGED
|
@@ -33,9 +33,6 @@ class BaseMarketRequest(Envelope):
|
|
|
33
33
|
# will be checked against the certificate used to make the request.
|
|
34
34
|
user: str = attr(name="UserName", min_length=1, max_length=12, pattern=r"^[A-Z0-9]*$")
|
|
35
35
|
|
|
36
|
-
# The type of market for which the data is being submitted
|
|
37
|
-
market_type: Optional[MarketType] = attr(default=None, name="MarketType")
|
|
38
|
-
|
|
39
36
|
|
|
40
37
|
class MarketQuery(BaseMarketRequest):
|
|
41
38
|
"""Represents the base fields for a market query."""
|
|
@@ -48,6 +45,9 @@ class MarketQuery(BaseMarketRequest):
|
|
|
48
45
|
class MarketSubmit(BaseMarketRequest):
|
|
49
46
|
"""Represents the base fields for a market registration request."""
|
|
50
47
|
|
|
48
|
+
# The type of market for which the data is being submitted
|
|
49
|
+
market_type: Optional[MarketType] = attr(default=None, name="MarketType")
|
|
50
|
+
|
|
51
51
|
# If the market type is specified as "DAM" (day-ahead market), the number of days should be specified as "1".
|
|
52
52
|
# Otherwise, this field indicates the number of days ahead for which the data is being submitted.
|
|
53
53
|
days: int = attr(default=1, name="NumOfDays", ge=1, le=31)
|
|
@@ -56,6 +56,9 @@ class MarketSubmit(BaseMarketRequest):
|
|
|
56
56
|
class MarketCancel(BaseMarketRequest):
|
|
57
57
|
"""Represents the base fields for a market cancellation request."""
|
|
58
58
|
|
|
59
|
+
# The type of market for which the data is being submitted
|
|
60
|
+
market_type: MarketType = attr(name="MarketType")
|
|
61
|
+
|
|
59
62
|
# If the market type is specified as "DAM" (day-ahead market), the number of days should be specified as "1".
|
|
60
63
|
# Otherwise, this field indicates the number of days ahead for which the data is being cancelled.
|
|
61
64
|
days: int = attr(default=1, name="NumOfDays", ge=1, le=31)
|
mms_client/types/offer.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
"""Contains objects for MMS offers."""
|
|
2
2
|
|
|
3
3
|
from decimal import Decimal
|
|
4
|
-
from enum import Enum
|
|
5
4
|
from typing import List
|
|
6
5
|
from typing import Optional
|
|
7
6
|
|
|
8
|
-
from pydantic_core import PydanticUndefined
|
|
9
7
|
from pydantic_extra_types.pendulum_dt import DateTime
|
|
10
8
|
from pydantic_xml import attr
|
|
11
9
|
from pydantic_xml import element
|
|
12
10
|
|
|
13
11
|
from mms_client.types.base import Payload
|
|
14
12
|
from mms_client.types.enums import AreaCode
|
|
13
|
+
from mms_client.types.enums import Direction
|
|
15
14
|
from mms_client.types.fields import company_short_name
|
|
16
15
|
from mms_client.types.fields import dr_patter_number
|
|
16
|
+
from mms_client.types.fields import offer_id
|
|
17
17
|
from mms_client.types.fields import operator_code
|
|
18
18
|
from mms_client.types.fields import participant
|
|
19
19
|
from mms_client.types.fields import power_positive
|
|
@@ -24,34 +24,6 @@ from mms_client.types.fields import system_code
|
|
|
24
24
|
from mms_client.types.market import MarketType
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def offer_id(alias: str, optional: bool = False):
|
|
28
|
-
"""Create a field for an offer ID.
|
|
29
|
-
|
|
30
|
-
Arguments:
|
|
31
|
-
alias (str): The name of the alias to assign to the Pydanitc field. This value will be used to map the field
|
|
32
|
-
to the JSON/XML key.
|
|
33
|
-
optional (bool): If True, the field will be optional with a default of None. If False, the field will be
|
|
34
|
-
required, with no default.
|
|
35
|
-
|
|
36
|
-
Returns: A Pydantic Field object for the offer ID.
|
|
37
|
-
"""
|
|
38
|
-
return attr(
|
|
39
|
-
default=None if optional else PydanticUndefined,
|
|
40
|
-
name=alias,
|
|
41
|
-
min_length=1,
|
|
42
|
-
max_length=30,
|
|
43
|
-
pattern=r"^[a-zA-Z0-9_-]*$",
|
|
44
|
-
)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class Direction(Enum):
|
|
48
|
-
"""Represents the reserve direction of the offer."""
|
|
49
|
-
|
|
50
|
-
SELL = "1" # Increasing the reserves (sell)
|
|
51
|
-
# Note: Support for the BUY direction was removed from the MMS API
|
|
52
|
-
# BUY = "2" # Decreasing the reserves (buy)
|
|
53
|
-
|
|
54
|
-
|
|
55
27
|
class OfferStack(Payload):
|
|
56
28
|
"""Represents a single price-quantity pair in an offer request.
|
|
57
29
|
|
|
@@ -94,7 +66,7 @@ class OfferStack(Payload):
|
|
|
94
66
|
tertiary_2_qty_kw: Optional[int] = power_positive("Tertiary2OfferQuantityInKw", True)
|
|
95
67
|
|
|
96
68
|
# The unit price of the power, in JPY/kW/segment
|
|
97
|
-
unit_price: Decimal = price("OfferUnitPrice")
|
|
69
|
+
unit_price: Decimal = price("OfferUnitPrice", 10000.00)
|
|
98
70
|
|
|
99
71
|
# The ID of the offer to which this stack belongs
|
|
100
72
|
id: Optional[str] = offer_id("OfferId", True)
|
mms_client/types/resource.py
CHANGED
|
@@ -16,7 +16,10 @@ from pydantic_xml import wrapped
|
|
|
16
16
|
|
|
17
17
|
from mms_client.types.base import Payload
|
|
18
18
|
from mms_client.types.enums import AreaCode
|
|
19
|
+
from mms_client.types.enums import BooleanFlag
|
|
20
|
+
from mms_client.types.enums import CommandMonitorMethod
|
|
19
21
|
from mms_client.types.enums import Frequency
|
|
22
|
+
from mms_client.types.enums import ResourceType
|
|
20
23
|
from mms_client.types.fields import ASCII_TEXT
|
|
21
24
|
from mms_client.types.fields import JAPANESE_ASCII_TEXT
|
|
22
25
|
from mms_client.types.fields import JAPANESE_TEXT
|
|
@@ -180,18 +183,6 @@ class ContractType(Enum):
|
|
|
180
183
|
REMAINING_RESERVE_UTILIZATION = "6"
|
|
181
184
|
|
|
182
185
|
|
|
183
|
-
class ResourceType(Enum):
|
|
184
|
-
"""How the power generation unit produces electricity."""
|
|
185
|
-
|
|
186
|
-
THERMAL = "01"
|
|
187
|
-
HYDRO = "02"
|
|
188
|
-
PUMP = "03"
|
|
189
|
-
BATTERY = "04"
|
|
190
|
-
VPP_GEN = "05"
|
|
191
|
-
VPP_GEN_AND_DEM = "06"
|
|
192
|
-
VPP_DEM = "07"
|
|
193
|
-
|
|
194
|
-
|
|
195
186
|
class RemainingReserveAvailability(Enum):
|
|
196
187
|
"""Describes the availability of remaining reserves for a power generation unit."""
|
|
197
188
|
|
|
@@ -201,14 +192,6 @@ class RemainingReserveAvailability(Enum):
|
|
|
201
192
|
AVAILABLE_FOR_UP_AND_DOWN = "3"
|
|
202
193
|
|
|
203
194
|
|
|
204
|
-
class CommandMonitorMethod(Enum):
|
|
205
|
-
"""Describes how the power generation unit is monitored and commanded."""
|
|
206
|
-
|
|
207
|
-
DEDICATED_LINE = "1"
|
|
208
|
-
SIMPLE_COMMAND = "2"
|
|
209
|
-
OFFLINE = "3"
|
|
210
|
-
|
|
211
|
-
|
|
212
195
|
class SignalType(Enum):
|
|
213
196
|
"""Describes the type of signal used to monitor and command a power generation unit."""
|
|
214
197
|
|
|
@@ -231,17 +214,6 @@ class ThermalType(Enum):
|
|
|
231
214
|
OTHER = "9"
|
|
232
215
|
|
|
233
216
|
|
|
234
|
-
class BooleanFlag(Enum):
|
|
235
|
-
"""Describes a boolean flag.
|
|
236
|
-
|
|
237
|
-
This could literally be a Boolean value but it's coded in the reference as an enum and I don't want to create a
|
|
238
|
-
custom serializer for it.
|
|
239
|
-
"""
|
|
240
|
-
|
|
241
|
-
YES = "1"
|
|
242
|
-
NO = "0"
|
|
243
|
-
|
|
244
|
-
|
|
245
217
|
class OverrideOption(Enum):
|
|
246
218
|
"""Describes the override option for a power generation unit."""
|
|
247
219
|
|
|
@@ -761,7 +733,7 @@ class ResourceData(Payload, tag="Resource"):
|
|
|
761
733
|
has_contract: Optional[BooleanFlag] = attr(default=None, name="ContractExistence")
|
|
762
734
|
|
|
763
735
|
# The maximum bid price for POWER_SUPPLY_1 power, in JPY/kW/hr
|
|
764
|
-
declared_maximum_unit_price_kWh: Annotated[Decimal, price("DeclaredMaximumUnitPrice", True)]
|
|
736
|
+
declared_maximum_unit_price_kWh: Annotated[Decimal, price("DeclaredMaximumUnitPrice", 10000.00, True)]
|
|
765
737
|
|
|
766
738
|
# Presence of voltage regulation function.
|
|
767
739
|
voltage_adjustable: Optional[BooleanFlag] = attr(default=None, name="VoltageAdjustment")
|
mms_client/types/transport.py
CHANGED
|
@@ -25,7 +25,7 @@ class RequestType(Enum):
|
|
|
25
25
|
class RequestDataType(Enum):
|
|
26
26
|
"""Represents the type of data to be sent to the MMS server.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
NOTE: JSON is currently not supported and has been left in for future use.
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
JSON = "JSON"
|
|
@@ -35,7 +35,7 @@ class RequestDataType(Enum):
|
|
|
35
35
|
class ResponseDataType(Enum):
|
|
36
36
|
"""Represents the type of data to be received from the MMS server.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
NOTE: JSON is currently not supported and has been left in for future use.
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
XML = "XML"
|
|
@@ -16,6 +16,7 @@ from lxml.etree import XMLSchema
|
|
|
16
16
|
from lxml.etree import _Element as Element
|
|
17
17
|
from lxml.etree import parse
|
|
18
18
|
from pydantic_xml import element
|
|
19
|
+
from pydantic_xml.typedefs import EntityLocation
|
|
19
20
|
|
|
20
21
|
from mms_client.types.base import E
|
|
21
22
|
from mms_client.types.base import Messages
|
|
@@ -69,12 +70,12 @@ class Serializer:
|
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
# Next, inject the payload and data into the payload class
|
|
72
|
-
#
|
|
73
|
+
# NOTE: this returns a type that inherits from PayloadBase and the arguments provided to the initializer
|
|
73
74
|
# here are correct, but mypy thinks they are incorrect because it doesn't understand the the inherited type
|
|
74
75
|
payload = payload_cls(request_envelope, request_data, self._xsd.value) # type: ignore[call-arg, misc]
|
|
75
76
|
|
|
76
77
|
# Finally, convert the payload to XML and return it
|
|
77
|
-
#
|
|
78
|
+
# NOTE: we provided the encoding here so this will return bytes, not a string
|
|
78
79
|
return payload.to_xml(skip_empty=True, encoding="utf-8", xml_declaration=True) # type: ignore[return-value]
|
|
79
80
|
|
|
80
81
|
def serialize_multi(self, request_envelope: E, request_data: List[P], request_type: Type[P]) -> bytes:
|
|
@@ -93,12 +94,12 @@ class Serializer:
|
|
|
93
94
|
)
|
|
94
95
|
|
|
95
96
|
# Next, inject the payload and data into the payload class
|
|
96
|
-
#
|
|
97
|
+
# NOTE: this returns a type that inherits from PayloadBase and the arguments provided to the initializer
|
|
97
98
|
# here are correct, but mypy thinks they are incorrect because it doesn't understand the the inherited type
|
|
98
99
|
payload = payload_cls(request_envelope, request_data, self._xsd.value) # type: ignore[call-arg, misc]
|
|
99
100
|
|
|
100
101
|
# Finally, convert the payload to XML and return it
|
|
101
|
-
#
|
|
102
|
+
# NOTE: we provided the encoding here so this will return bytes, not a string
|
|
102
103
|
return payload.to_xml(skip_empty=True, encoding="utf-8", xml_declaration=True) # type: ignore[return-value]
|
|
103
104
|
|
|
104
105
|
def deserialize(self, data: bytes, envelope_type: Type[E], data_type: Type[P]) -> Response[E, P]:
|
|
@@ -190,7 +191,7 @@ class Serializer:
|
|
|
190
191
|
|
|
191
192
|
# Now, verify that the response doesn't contain an unexpected data type and then retrieve the payload data
|
|
192
193
|
# from within the envelope
|
|
193
|
-
#
|
|
194
|
+
# NOTE: apparently, mypy doesn't know about setter-getter properties either...
|
|
194
195
|
self._verify_tree_data_tag(env_node, data_type)
|
|
195
196
|
resp.payload = [
|
|
196
197
|
self._from_tree_data(item, data_type) for item in env_node.findall(get_tag(data_type)) # type: ignore[misc]
|
|
@@ -271,7 +272,13 @@ class Serializer:
|
|
|
271
272
|
)
|
|
272
273
|
|
|
273
274
|
def _from_tree_messages(
|
|
274
|
-
self,
|
|
275
|
+
self,
|
|
276
|
+
raw: Element,
|
|
277
|
+
envelope_type: Type[E],
|
|
278
|
+
current_type: Type[P],
|
|
279
|
+
root: str,
|
|
280
|
+
multi: bool,
|
|
281
|
+
wrapped: bool = False,
|
|
275
282
|
) -> Dict[str, Messages]:
|
|
276
283
|
"""Attempt to extract the messages from within the payload.
|
|
277
284
|
|
|
@@ -287,6 +294,7 @@ class Serializer:
|
|
|
287
294
|
multi (bool): Whether we're processing a list of nodes or a single node. If called with the
|
|
288
295
|
payload root, this value will determine whether we're processing a multi-
|
|
289
296
|
response or a single response.
|
|
297
|
+
wrapped (bool): Whether or not this type is referenced from a wrapped field.
|
|
290
298
|
"""
|
|
291
299
|
# First, find the Messages node in the raw data
|
|
292
300
|
message_node = raw.find("Messages")
|
|
@@ -309,40 +317,41 @@ class Serializer:
|
|
|
309
317
|
current_type,
|
|
310
318
|
f"{root}.{envelope_type.__name__}",
|
|
311
319
|
multi,
|
|
320
|
+
False,
|
|
312
321
|
)
|
|
313
322
|
)
|
|
314
|
-
elif root.endswith(envelope_type.__name__):
|
|
323
|
+
elif root.endswith(envelope_type.__name__) or wrapped:
|
|
315
324
|
messages.update(
|
|
316
|
-
self._from_tree_messages_inner(
|
|
325
|
+
self._from_tree_messages_inner(
|
|
326
|
+
raw, envelope_type, current_type, root, get_tag(current_type), multi, False
|
|
327
|
+
)
|
|
317
328
|
)
|
|
318
329
|
else:
|
|
319
330
|
# Iterate over each field on the current type...
|
|
320
331
|
for field in current_type.model_fields.values():
|
|
321
332
|
|
|
322
|
-
# First, get the arguments and origin of the field's annotation
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
has_args = len(args) > 0
|
|
333
|
+
# First, get the arguments and origin of the field's annotation. Occaisionally, we'll have an optional
|
|
334
|
+
# list. In this case, we'll have to do get_args twice to traverse the type tree.
|
|
335
|
+
arg, multi = _get_field_typing(field.annotation) # type: ignore[arg-type]
|
|
326
336
|
|
|
327
337
|
# Next, check if the annotation is a subclass of Payload or else if it's a collection of Payload. If
|
|
328
338
|
# neither of these is the case, we can skip this field.
|
|
329
|
-
#
|
|
330
|
-
if not (
|
|
331
|
-
(has_args and issubclass(args[0], Payload))
|
|
332
|
-
or (not has_args and issubclass(field.annotation, Payload)) # type: ignore[arg-type]
|
|
333
|
-
):
|
|
339
|
+
# NOTE: All our fields are annotated so there's no need to check if they're not
|
|
340
|
+
if not issubclass(arg, Payload):
|
|
334
341
|
continue
|
|
335
342
|
|
|
336
343
|
# Finally, call this method recursively for the field and update the messages with the results
|
|
337
|
-
#
|
|
344
|
+
# NOTE: All our fields are annotated as XmlEntityInfo, so they have the "path" and "location" attributes
|
|
345
|
+
print(field)
|
|
338
346
|
messages.update(
|
|
339
347
|
self._from_tree_messages_inner(
|
|
340
348
|
raw,
|
|
341
349
|
envelope_type,
|
|
342
|
-
|
|
350
|
+
arg,
|
|
343
351
|
root,
|
|
344
352
|
field.path, # type: ignore[attr-defined]
|
|
345
|
-
|
|
353
|
+
multi,
|
|
354
|
+
field.location == EntityLocation.WRAPPED, # type: ignore[attr-defined]
|
|
346
355
|
)
|
|
347
356
|
)
|
|
348
357
|
|
|
@@ -350,7 +359,14 @@ class Serializer:
|
|
|
350
359
|
return messages
|
|
351
360
|
|
|
352
361
|
def _from_tree_messages_inner(
|
|
353
|
-
self,
|
|
362
|
+
self,
|
|
363
|
+
raw: Element,
|
|
364
|
+
envelope_type: Type[E],
|
|
365
|
+
current_type: Type[P],
|
|
366
|
+
root: str,
|
|
367
|
+
tag: str,
|
|
368
|
+
multi: bool,
|
|
369
|
+
wrapped: bool,
|
|
354
370
|
) -> Dict[str, Messages]:
|
|
355
371
|
"""Attempt to extract the messages from within the payload at the current level.
|
|
356
372
|
|
|
@@ -362,6 +378,7 @@ class Serializer:
|
|
|
362
378
|
tag (str): The tag of the current node being processed.
|
|
363
379
|
multi (bool): If True, the payload will be a multi-response; otherwise, it will be a single
|
|
364
380
|
response.
|
|
381
|
+
wrapped (bool): Whether or not this field is a wrapped field.
|
|
365
382
|
|
|
366
383
|
Returns: A dictionary mapping messages to where they were found in the response.
|
|
367
384
|
"""
|
|
@@ -379,12 +396,20 @@ class Serializer:
|
|
|
379
396
|
# Otherwise, we'll call this method recursively for each node and update the messages with the results.
|
|
380
397
|
messages = {}
|
|
381
398
|
for i, node in enumerate(nodes):
|
|
382
|
-
messages.update(
|
|
399
|
+
messages.update(
|
|
400
|
+
self._from_tree_messages(
|
|
401
|
+
node, envelope_type, current_type, path_base if wrapped else f"{path_base}[{i}]", True, wrapped
|
|
402
|
+
)
|
|
403
|
+
)
|
|
383
404
|
return messages
|
|
384
405
|
|
|
385
406
|
# If we reached this point then we are processing a single item so find the associated
|
|
386
407
|
child = raw.find(tag)
|
|
387
|
-
return
|
|
408
|
+
return (
|
|
409
|
+
{}
|
|
410
|
+
if child is None
|
|
411
|
+
else self._from_tree_messages(child, envelope_type, current_type, path_base, False, wrapped)
|
|
412
|
+
)
|
|
388
413
|
|
|
389
414
|
def _from_xml(self, data: bytes) -> Element:
|
|
390
415
|
"""Parse the XML file, returning the resulting XML tree.
|
|
@@ -547,6 +572,45 @@ def _find_or_fail(node: Element, tag: str) -> Element:
|
|
|
547
572
|
return found
|
|
548
573
|
|
|
549
574
|
|
|
575
|
+
def _get_field_typing(typ: Type) -> Tuple[Type, bool]:
|
|
576
|
+
"""Retrieve the field's actual type and whether or not the field is a collection.
|
|
577
|
+
|
|
578
|
+
This method is designed to find the inner type of fields in the following cases:
|
|
579
|
+
1. Fundamental types and classes (e.g. int, str, Award, Offer, etc.)
|
|
580
|
+
2. Nullable fundamental types and classes
|
|
581
|
+
3. Collections of fundamental types and classes
|
|
582
|
+
4. Nullable collections of fundamental types and classes
|
|
583
|
+
|
|
584
|
+
Arguments:
|
|
585
|
+
typ (Type): The type of the field to retrieve the inner type for.
|
|
586
|
+
|
|
587
|
+
Returns:
|
|
588
|
+
Type: The inner type of the field.
|
|
589
|
+
bool: Whether or not the field is a collection.
|
|
590
|
+
"""
|
|
591
|
+
# First, check for the case where we have a fundamental type. If we do then we can return the type and False.
|
|
592
|
+
init = get_args(typ)
|
|
593
|
+
if len(init) == 0:
|
|
594
|
+
return typ, False
|
|
595
|
+
|
|
596
|
+
# Next, iterate over the type hierarchy and repeat the operation until we find the leaf type.
|
|
597
|
+
args = [get_args(typ)]
|
|
598
|
+
while len(args[-1]) > 1:
|
|
599
|
+
temp = get_args(args[-1][0])
|
|
600
|
+
if len(temp) == 0:
|
|
601
|
+
break
|
|
602
|
+
args.append(temp)
|
|
603
|
+
|
|
604
|
+
# Now, find the origin type of the field. This will be the lowest type in the hierarchy that isn't a multi-type.
|
|
605
|
+
# If there aren't any of these, then we'll just use the original type.
|
|
606
|
+
origin_type = next(
|
|
607
|
+
filter(lambda x: x is not None, map(lambda arg: arg[0] if len(arg) > 1 else None, reversed(args))), typ
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# Finally, return the inner type and whether or not the origin type is a list
|
|
611
|
+
return args[-1][0] if len(args) > 0 else typ, get_origin(origin_type) is list # typing: ignore[return-value]
|
|
612
|
+
|
|
613
|
+
|
|
550
614
|
def get_tag(data_type: Type[P]) -> str:
|
|
551
615
|
"""Get the tag for the given data type.
|
|
552
616
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mms-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
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
|
|
@@ -174,14 +174,14 @@ There's a lot of code here but it's not terribly difficult to understand. All th
|
|
|
174
174
|
If you want to test your MMS connection, you can try using the test server:
|
|
175
175
|
|
|
176
176
|
```python
|
|
177
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert,
|
|
177
|
+
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, test=True)
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
## Connecting as a Market Admin
|
|
181
181
|
If you're connecting as a market operator (MO), you can connect in admin mode:
|
|
182
182
|
|
|
183
183
|
```python
|
|
184
|
-
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert,
|
|
184
|
+
client = MmsClient(participant="F100", user="FAKEUSER", client_type=ClientType.BSP, cert, is_admin=True)
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
## Log Settings
|
|
@@ -198,6 +198,7 @@ This client is not complete. Currently, it supports the following endpoints:
|
|
|
198
198
|
- MarketSubmit_OfferData
|
|
199
199
|
- MarketQuery_OfferQuery
|
|
200
200
|
- MarketCancel_OfferCancel
|
|
201
|
+
- MarketQuery_AwardResultsQuery
|
|
201
202
|
- RegistrationSubmit_Resource
|
|
202
203
|
- RegistrationQuery_Resource
|
|
203
204
|
|
|
@@ -13,24 +13,25 @@ mms_client/security/certs.py,sha256=kNCUFmy18YIxkWKu3mdMmlxmHdft4a6BvtyJ46rA9I4,
|
|
|
13
13
|
mms_client/security/crypto.py,sha256=M7aIllM3_ZwZgm9nH6QQ6Ig14XCAd6e6WGwqqUbbI1Q,2149
|
|
14
14
|
mms_client/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
mms_client/services/base.py,sha256=mcUL-AZpaJETVkzAOoazmKfI5O3cJ0KWzIYQ5bjpNuE,25839
|
|
16
|
-
mms_client/services/market.py,sha256=
|
|
16
|
+
mms_client/services/market.py,sha256=aIWi52BFvDmtuipKnSZNprwbFYGYBcV6sZCQqMR_D2g,7574
|
|
17
17
|
mms_client/services/omi.py,sha256=h6cM5U3-iSm0YiIaJwqYTZeI5uhLbA7FPxh_qy_3qww,521
|
|
18
|
-
mms_client/services/registration.py,sha256=
|
|
18
|
+
mms_client/services/registration.py,sha256=46Scntwlc9CtCO-tV6uEnr4NrVGJiUqhvksII13CGAE,3651
|
|
19
19
|
mms_client/services/report.py,sha256=HYVJNwEHo6ZC6497UqO5y1IqZ2ga3kVH5AepdxhYfug,537
|
|
20
20
|
mms_client/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
mms_client/types/
|
|
22
|
-
mms_client/types/
|
|
23
|
-
mms_client/types/
|
|
24
|
-
mms_client/types/
|
|
25
|
-
mms_client/types/
|
|
21
|
+
mms_client/types/award.py,sha256=BWE9V_KHXpg_cW1LZsetVrPs2hZDOklvRpNnoZtmR3k,14139
|
|
22
|
+
mms_client/types/base.py,sha256=VQCr50CL1SnnPcO1EYGHa4rrkHBtXs-J8psWLJWoHJY,9710
|
|
23
|
+
mms_client/types/enums.py,sha256=YJ58FbhyQ0TVlf8Z-Dg1UfVu8CurY5b21Cy5-WYkJ0I,1629
|
|
24
|
+
mms_client/types/fields.py,sha256=pa5qvQVwEr8dh44IGHyYqgJYTYyTIeAjBW6CylXrkP0,14785
|
|
25
|
+
mms_client/types/market.py,sha256=IbXsH4Q5MJI-CEvGvZlzv2S36mX_Ea02U11Ik-NwSxQ,2706
|
|
26
|
+
mms_client/types/offer.py,sha256=KosFiKRMnt7XwlLBUfjHUGHiWzrMJUPPhGQMxgdeepM,6791
|
|
26
27
|
mms_client/types/registration.py,sha256=Nir73S3ffpk0O_fnTD2alFaqV1k67_8dcyyduXvPBI4,1381
|
|
27
|
-
mms_client/types/resource.py,sha256=
|
|
28
|
-
mms_client/types/transport.py,sha256=
|
|
28
|
+
mms_client/types/resource.py,sha256=TQnY3JLHRgQhQrG6ISquw-BQgKSr8TGuqn9ItWxWz_w,65974
|
|
29
|
+
mms_client/types/transport.py,sha256=DPjWs34UW915GkUCJWKuDZmsjS6mRdRXgcGISduN_Bc,4399
|
|
29
30
|
mms_client/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
31
|
mms_client/utils/errors.py,sha256=jYdlG4OPI82s0fJcXCRNlKeEixDUSSAxjs_7C16qVL4,2306
|
|
31
|
-
mms_client/utils/serialization.py,sha256=
|
|
32
|
+
mms_client/utils/serialization.py,sha256=vTJZSjhfTph2tDAYbLAhnz89i0qXJ8FINGdve67cJOU,29534
|
|
32
33
|
mms_client/utils/web.py,sha256=fcdCtdDrHBPyhIlTcyiuAk3D3TlW8HmUw-wGfpG4KTA,9653
|
|
33
|
-
mms_client-1.
|
|
34
|
-
mms_client-1.
|
|
35
|
-
mms_client-1.
|
|
36
|
-
mms_client-1.
|
|
34
|
+
mms_client-1.3.1.dist-info/LICENSE,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
|
|
35
|
+
mms_client-1.3.1.dist-info/METADATA,sha256=oSdqfUgW01Tr2ZRcCUd4SedQzQZrvNLY5fWmNmlMviY,14774
|
|
36
|
+
mms_client-1.3.1.dist-info/WHEEL,sha256=Zb28QaM1gQi8f4VCBhsUklF61CTlNYfs9YAZn-TOGFk,88
|
|
37
|
+
mms_client-1.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|