mms-client 1.6.0__py3-none-any.whl → 1.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mms_client/security/certs.py +34 -1
- mms_client/security/crypto.py +30 -22
- mms_client/services/base.py +133 -66
- mms_client/services/market.py +31 -12
- mms_client/services/registration.py +12 -9
- mms_client/services/report.py +144 -1
- mms_client/types/base.py +53 -13
- mms_client/types/enums.py +34 -0
- mms_client/types/report.py +474 -0
- mms_client/types/resource.py +4 -34
- mms_client/types/transport.py +2 -2
- mms_client/utils/multipart_transport.py +259 -0
- mms_client/utils/serialization.py +137 -44
- mms_client/utils/web.py +16 -2
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/METADATA +14 -5
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/RECORD +18 -16
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/LICENSE +0 -0
- {mms_client-1.6.0.dist-info → mms_client-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -10,7 +10,6 @@ from mms_client.services.base import ServiceConfiguration
|
|
|
10
10
|
from mms_client.services.base import mms_endpoint
|
|
11
11
|
from mms_client.services.base import mms_multi_endpoint
|
|
12
12
|
from mms_client.types.registration import QueryAction
|
|
13
|
-
from mms_client.types.registration import QueryType
|
|
14
13
|
from mms_client.types.registration import RegistrationQuery
|
|
15
14
|
from mms_client.types.registration import RegistrationSubmit
|
|
16
15
|
from mms_client.types.resource import ResourceData
|
|
@@ -31,7 +30,12 @@ class RegistrationClientMixin: # pylint: disable=unused-argument
|
|
|
31
30
|
# The configuration for the registration service
|
|
32
31
|
config = ServiceConfiguration(Interface.MI, Serializer(SchemaType.REGISTRATION, "RegistrationData"))
|
|
33
32
|
|
|
34
|
-
@mms_endpoint(
|
|
33
|
+
@mms_endpoint(
|
|
34
|
+
name="RegistrationSubmit_Resource",
|
|
35
|
+
service=config,
|
|
36
|
+
request_type=RequestType.REGISTRATION,
|
|
37
|
+
allowed_clients=[ClientType.BSP],
|
|
38
|
+
)
|
|
35
39
|
def put_resource(self: ClientProto, request: ResourceData) -> ResourceData:
|
|
36
40
|
"""Submit a new resource to the MMS server.
|
|
37
41
|
|
|
@@ -51,12 +55,12 @@ class RegistrationClientMixin: # pylint: disable=unused-argument
|
|
|
51
55
|
return RegistrationSubmit() # type: ignore[return-value]
|
|
52
56
|
|
|
53
57
|
@mms_multi_endpoint(
|
|
54
|
-
"RegistrationQuery_Resource",
|
|
55
|
-
config,
|
|
56
|
-
RequestType.REGISTRATION,
|
|
58
|
+
name="RegistrationQuery_Resource",
|
|
59
|
+
service=config,
|
|
60
|
+
request_type=RequestType.REGISTRATION,
|
|
57
61
|
allowed_clients=[ClientType.BSP, ClientType.TSO],
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
response_envelope_type=RegistrationSubmit,
|
|
63
|
+
response_data_type=ResourceData,
|
|
60
64
|
)
|
|
61
65
|
def query_resources(
|
|
62
66
|
self: ClientProto, request: ResourceQuery, action: QueryAction, date: Optional[Date] = None
|
|
@@ -79,6 +83,5 @@ class RegistrationClientMixin: # pylint: disable=unused-argument
|
|
|
79
83
|
# Inject our parameters into the query and return it.
|
|
80
84
|
return RegistrationQuery( # type: ignore[return-value]
|
|
81
85
|
action=action,
|
|
82
|
-
|
|
83
|
-
date=date or Date.today(),
|
|
86
|
+
date=date,
|
|
84
87
|
)
|
mms_client/services/report.py
CHANGED
|
@@ -1,18 +1,161 @@
|
|
|
1
1
|
"""Contains the client layer for making report requests to the MMS server."""
|
|
2
2
|
|
|
3
3
|
from logging import getLogger
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import List
|
|
6
|
+
from typing import Type
|
|
7
|
+
from typing import TypeVar
|
|
4
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.base import Response
|
|
14
|
+
from mms_client.types.report import ApplicationType
|
|
15
|
+
from mms_client.types.report import BSPResourceListItem
|
|
16
|
+
from mms_client.types.report import ListReportRequest
|
|
17
|
+
from mms_client.types.report import ListReportResponse
|
|
18
|
+
from mms_client.types.report import NewReportRequest
|
|
19
|
+
from mms_client.types.report import NewReportResponse
|
|
20
|
+
from mms_client.types.report import OutboundData
|
|
21
|
+
from mms_client.types.report import ReportBase
|
|
22
|
+
from mms_client.types.report import ReportDownloadRequestTrnID
|
|
23
|
+
from mms_client.types.report import ReportLineBase
|
|
24
|
+
from mms_client.types.transport import RequestType
|
|
6
25
|
from mms_client.utils.serialization import SchemaType
|
|
7
26
|
from mms_client.utils.serialization import Serializer
|
|
27
|
+
from mms_client.utils.web import ClientType
|
|
8
28
|
from mms_client.utils.web import Interface
|
|
9
29
|
|
|
10
30
|
# Set the default logger for the MMS client
|
|
11
31
|
logger = getLogger(__name__)
|
|
12
32
|
|
|
13
33
|
|
|
34
|
+
# The type variable we'll use to represent report line items
|
|
35
|
+
R = TypeVar("R", bound=ReportLineBase)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def attach_transaction_id(
|
|
39
|
+
resp: Response[ReportBase, NewReportResponse],
|
|
40
|
+
attachments: Dict[str, bytes], # pylint: disable=unused-argument
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Attach the transaction ID to the response.
|
|
43
|
+
|
|
44
|
+
Arguments:
|
|
45
|
+
resp (Response[ReportBase, NewReportResponse]): The MMS response.
|
|
46
|
+
attachments (Dict[str, bytes]): Attachements to the response.
|
|
47
|
+
"""
|
|
48
|
+
if resp.data and resp.statistics is not None:
|
|
49
|
+
resp.data.transaction_id = resp.statistics.transaction_id or ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def report_getter_factory(config: ServiceConfiguration, resp_data_type: Type[R], allowed_clients: List[ClientType]):
|
|
53
|
+
"""Create a function for getting a report with a transaction ID.
|
|
54
|
+
|
|
55
|
+
Arguments:
|
|
56
|
+
config (ServiceConfiguration): The configuration for the report service.
|
|
57
|
+
resp_data_type (Type[R]): The type of report data to return.
|
|
58
|
+
allowed_clients (List[ClientType]): The allowed client types for the report service.
|
|
59
|
+
|
|
60
|
+
Returns: A function for getting a report with a transaction ID.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@mms_multi_endpoint(
|
|
64
|
+
name="ReportDownloadRequestTrnID",
|
|
65
|
+
service=config,
|
|
66
|
+
request_type=RequestType.REPORT,
|
|
67
|
+
allowed_clients=allowed_clients,
|
|
68
|
+
response_envelope_type=OutboundData,
|
|
69
|
+
response_data_type=resp_data_type,
|
|
70
|
+
serializer=Serializer(SchemaType.REPORT_RESPONSE, "OutboundData"),
|
|
71
|
+
for_report=True,
|
|
72
|
+
)
|
|
73
|
+
def get_report_with_transaction_id(
|
|
74
|
+
self: ClientProto,
|
|
75
|
+
request: ReportDownloadRequestTrnID, # pylint: disable=unused-argument
|
|
76
|
+
) -> List[resp_data_type]: # type: ignore[valid-type]
|
|
77
|
+
"""Request download of a report with a transaction ID.
|
|
78
|
+
|
|
79
|
+
This endpoint is accessible to any client, but may be rejected depending on the type of report being requested.
|
|
80
|
+
|
|
81
|
+
Arguments:
|
|
82
|
+
self (ClientProto): The client to use for making the request.
|
|
83
|
+
request (ReportDownloadRequestTrnID): The request to download the report.
|
|
84
|
+
|
|
85
|
+
Returns: The request to download the report.
|
|
86
|
+
"""
|
|
87
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
88
|
+
return ReportBase( # type: ignore[return-value]
|
|
89
|
+
application_type=ApplicationType.MARKET_REPORT,
|
|
90
|
+
participant=self.participant,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return get_report_with_transaction_id
|
|
94
|
+
|
|
95
|
+
|
|
14
96
|
class ReportClientMixin: # pylint: disable=unused-argument
|
|
15
97
|
"""Report client for the MMS server."""
|
|
16
98
|
|
|
17
99
|
# The configuration for the report service
|
|
18
|
-
config = ServiceConfiguration(
|
|
100
|
+
config = ServiceConfiguration(
|
|
101
|
+
Interface.MI,
|
|
102
|
+
Serializer(SchemaType.REPORT, "MarketReport"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
@mms_endpoint(
|
|
106
|
+
name="ReportCreateRequest",
|
|
107
|
+
service=config,
|
|
108
|
+
request_type=RequestType.REPORT,
|
|
109
|
+
response_envelope_type=ReportBase,
|
|
110
|
+
response_data_type=NewReportResponse,
|
|
111
|
+
for_report=True,
|
|
112
|
+
)
|
|
113
|
+
def create_report(self: ClientProto, request: NewReportRequest) -> NewReportResponse:
|
|
114
|
+
"""Request creation of a new report.
|
|
115
|
+
|
|
116
|
+
This endpoint is only accessible to BSPs.
|
|
117
|
+
|
|
118
|
+
Arguments:
|
|
119
|
+
request (NewReportRequest): The request to create the report.
|
|
120
|
+
|
|
121
|
+
Returns: The request to create the report.
|
|
122
|
+
"""
|
|
123
|
+
# If the client type is BSP, then we can't set the BSP name
|
|
124
|
+
if self.client_type == ClientType.BSP:
|
|
125
|
+
request.bsp_name = None
|
|
126
|
+
|
|
127
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
128
|
+
# Return the envelope and the callback function
|
|
129
|
+
return (
|
|
130
|
+
ReportBase( # type: ignore[return-value]
|
|
131
|
+
application_type=ApplicationType.MARKET_REPORT,
|
|
132
|
+
participant=self.participant,
|
|
133
|
+
),
|
|
134
|
+
attach_transaction_id,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@mms_endpoint(
|
|
138
|
+
name="ReportListRequest",
|
|
139
|
+
service=config,
|
|
140
|
+
request_type=RequestType.REPORT,
|
|
141
|
+
response_envelope_type=ReportBase,
|
|
142
|
+
response_data_type=ListReportResponse,
|
|
143
|
+
for_report=True,
|
|
144
|
+
)
|
|
145
|
+
def list_reports(self: ClientProto, request: ListReportRequest) -> ListReportResponse:
|
|
146
|
+
"""Query the existing reports.
|
|
147
|
+
|
|
148
|
+
Arguments:
|
|
149
|
+
request (ListReportRequest): The request to query the reports.
|
|
150
|
+
|
|
151
|
+
Returns: A list of reports that match the query.
|
|
152
|
+
"""
|
|
153
|
+
# NOTE: The return type does not match the method definition but the decorator will return the correct type
|
|
154
|
+
# Return the envelope and the callback function
|
|
155
|
+
return ReportBase( # type: ignore[return-value]
|
|
156
|
+
application_type=ApplicationType.MARKET_REPORT,
|
|
157
|
+
participant=self.participant,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Define the individual report getter functions here
|
|
161
|
+
list_bsp_resources = report_getter_factory(config, BSPResourceListItem, [ClientType.BSP])
|
mms_client/types/base.py
CHANGED
|
@@ -8,10 +8,12 @@ from typing import List
|
|
|
8
8
|
from typing import Optional
|
|
9
9
|
from typing import TypeVar
|
|
10
10
|
|
|
11
|
+
from lxml.etree import _Element as Element
|
|
11
12
|
from pydantic import PrivateAttr
|
|
12
13
|
from pydantic_extra_types.pendulum_dt import DateTime
|
|
13
14
|
from pydantic_xml import BaseXmlModel
|
|
14
15
|
from pydantic_xml import attr
|
|
16
|
+
from pydantic_xml import computed_element
|
|
15
17
|
from pydantic_xml import element
|
|
16
18
|
|
|
17
19
|
from mms_client.types.fields import transaction_id
|
|
@@ -34,17 +36,48 @@ class Message(BaseXmlModel):
|
|
|
34
36
|
code: str = attr(default="", name="Code", min_length=2, max_length=50, pattern=r"^[a-zA-Z_0-9\-]*$")
|
|
35
37
|
|
|
36
38
|
|
|
37
|
-
class Messages(BaseXmlModel, search_mode="unordered"):
|
|
39
|
+
class Messages(BaseXmlModel, search_mode="unordered", arbitrary_types_allowed=True):
|
|
38
40
|
"""Represents a collection of messages returned with a payload."""
|
|
39
41
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
+
# The raw information messages returned with a payload
|
|
43
|
+
information_raw: List[Element] = element(default=[], tag="Information", nillable=True, exclude=True)
|
|
42
44
|
|
|
43
|
-
#
|
|
44
|
-
|
|
45
|
+
# The raw warning messages returned with a payload
|
|
46
|
+
warnings_raw: List[Element] = element(default=[], tag="Warning", nillable=True, exclude=True)
|
|
45
47
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
+
# The raw error messages returned with a payload
|
|
49
|
+
errors_raw: List[Element] = element(default=[], tag="Error", nillable=True, exclude=True)
|
|
50
|
+
|
|
51
|
+
@computed_element
|
|
52
|
+
def information(self) -> List[str]:
|
|
53
|
+
"""Return the information messages."""
|
|
54
|
+
return self._parse_messages(self.information_raw)
|
|
55
|
+
|
|
56
|
+
@computed_element
|
|
57
|
+
def warnings(self) -> List[str]:
|
|
58
|
+
"""Return the warning messages."""
|
|
59
|
+
return self._parse_messages(self.warnings_raw)
|
|
60
|
+
|
|
61
|
+
@computed_element
|
|
62
|
+
def errors(self) -> List[str]:
|
|
63
|
+
"""Return the error messages."""
|
|
64
|
+
return self._parse_messages(self.errors_raw)
|
|
65
|
+
|
|
66
|
+
def _parse_messages(self, raw: List[Element]) -> List[str]:
|
|
67
|
+
"""Parse the messages from the XML tree.
|
|
68
|
+
|
|
69
|
+
Arguments:
|
|
70
|
+
raw (List[Element]): The raw XML tree to parse.
|
|
71
|
+
|
|
72
|
+
Returns: A list of message codes.
|
|
73
|
+
"""
|
|
74
|
+
messages = []
|
|
75
|
+
for item in raw:
|
|
76
|
+
if message := item.attrib.get("Code"):
|
|
77
|
+
messages.append(message)
|
|
78
|
+
else:
|
|
79
|
+
messages.append(item.text or "")
|
|
80
|
+
return messages
|
|
48
81
|
|
|
49
82
|
|
|
50
83
|
class ProcessingStatistics(BaseXmlModel):
|
|
@@ -87,7 +120,16 @@ class ResponseCommon(BaseXmlModel, search_mode="unordered"):
|
|
|
87
120
|
|
|
88
121
|
# The status of the validation check done on the element. This field is not required for requests, and will be
|
|
89
122
|
# populated in responses. For responses, the default value is "NOT_DONE".
|
|
90
|
-
|
|
123
|
+
base_validation: Optional[ValidationStatus] = attr(default=None, name="Validation")
|
|
124
|
+
|
|
125
|
+
# The status of the validation check done on the element, specifically for reports. This field is not required for
|
|
126
|
+
# requests, and will be populated in responses. For responses, the default value is "NOT_DONE".
|
|
127
|
+
report_validation: Optional[ValidationStatus] = attr(default=None, name="ValidationStatus")
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def validation(self) -> ValidationStatus:
|
|
131
|
+
"""Return the validation status of the element."""
|
|
132
|
+
return self.base_validation or self.report_validation or ValidationStatus.NOT_DONE
|
|
91
133
|
|
|
92
134
|
|
|
93
135
|
class Payload(BaseXmlModel, search_mode="unordered"):
|
|
@@ -111,23 +153,21 @@ class SchemaType(Enum):
|
|
|
111
153
|
|
|
112
154
|
MARKET = "mi-market.xsd"
|
|
113
155
|
REPORT = "mi-report.xsd"
|
|
156
|
+
REPORT_RESPONSE = "mi-outbnd-reports.xsd"
|
|
114
157
|
REGISTRATION = "mpr.xsd"
|
|
115
158
|
OMI = "omi.xsd"
|
|
116
159
|
|
|
117
160
|
|
|
118
|
-
class PayloadBase(BaseXmlModel
|
|
161
|
+
class PayloadBase(BaseXmlModel):
|
|
119
162
|
"""Represents the base fields for an MMS request payload."""
|
|
120
163
|
|
|
121
|
-
# The XML schema to use for validation
|
|
122
|
-
location: SchemaType = attr(name="noNamespaceSchemaLocation", ns="xsi")
|
|
123
|
-
|
|
124
164
|
|
|
125
165
|
class BaseResponse(BaseXmlModel, Generic[E], tag="BaseResponse"):
|
|
126
166
|
"""Contains the base data extracted from the MMS response in a format we can use."""
|
|
127
167
|
|
|
128
168
|
# The processing statistics returned with the payload. This will not be present in requests, and will be populated
|
|
129
169
|
# in responses.
|
|
130
|
-
statistics: ProcessingStatistics = element(tag="ProcessingStatistics")
|
|
170
|
+
statistics: Optional[ProcessingStatistics] = element(default=None, tag="ProcessingStatistics")
|
|
131
171
|
|
|
132
172
|
# The request payload, containing the request data
|
|
133
173
|
_envelope: E = PrivateAttr()
|
mms_client/types/enums.py
CHANGED
|
@@ -53,6 +53,26 @@ class ResourceType(Enum):
|
|
|
53
53
|
VPP_DEM = "07"
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
class RemainingReserveAvailability(Enum):
|
|
57
|
+
"""Describes the availability of remaining reserves for a power generation unit."""
|
|
58
|
+
|
|
59
|
+
NOT_AVAILABLE = "0"
|
|
60
|
+
AVAILABLE_FOR_UP_ONLY = "1"
|
|
61
|
+
AVAILABLE_FOR_DOWN_ONLY = "2"
|
|
62
|
+
AVAILABLE_FOR_UP_AND_DOWN = "3"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ContractType(Enum):
|
|
66
|
+
"""Describes the type of contract for a power generation unit."""
|
|
67
|
+
|
|
68
|
+
MARKET = "1"
|
|
69
|
+
MARKET_AND_POWER_SUPPLY_2 = "2"
|
|
70
|
+
POWER_SUPPLY_2 = "3"
|
|
71
|
+
ONLY_POWER_SUPPLY_1 = "4"
|
|
72
|
+
MARKET_AND_REMAINING_RESERVE_UTILIZATION = "5"
|
|
73
|
+
REMAINING_RESERVE_UTILIZATION = "6"
|
|
74
|
+
|
|
75
|
+
|
|
56
76
|
class CommandMonitorMethod(Enum):
|
|
57
77
|
"""Describes how the power generation unit is monitored and commanded."""
|
|
58
78
|
|
|
@@ -61,6 +81,20 @@ class CommandMonitorMethod(Enum):
|
|
|
61
81
|
OFFLINE = "3"
|
|
62
82
|
|
|
63
83
|
|
|
84
|
+
class BaseLineSettingMethod(Enum):
|
|
85
|
+
"""Describe how the baseline is set for a power generation unit."""
|
|
86
|
+
|
|
87
|
+
PREDICTION_BASE = "1"
|
|
88
|
+
MEASUREMENT_BASE = "2"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class SignalType(Enum):
|
|
92
|
+
"""Describes the type of signal used to monitor and command a power generation unit."""
|
|
93
|
+
|
|
94
|
+
ACTUAL_OUTPUT_ORDER = "1"
|
|
95
|
+
DIFFERENTIAL_OUTPUT_ORDER = "2"
|
|
96
|
+
|
|
97
|
+
|
|
64
98
|
class BooleanFlag(Enum):
|
|
65
99
|
"""Represents a Boolean value as an enumeration.
|
|
66
100
|
|