mms-client 1.0.5__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.
@@ -0,0 +1,163 @@
1
+ """Contains objects for MMS offers."""
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
+
13
+ from mms_client.types.base import Payload
14
+ from mms_client.types.enums import AreaCode
15
+ from mms_client.types.fields import company_short_name
16
+ from mms_client.types.fields import dr_patter_number
17
+ from mms_client.types.fields import operator_code
18
+ from mms_client.types.fields import participant
19
+ from mms_client.types.fields import power_positive
20
+ from mms_client.types.fields import price
21
+ from mms_client.types.fields import resource_name
22
+ from mms_client.types.fields import resource_short_name
23
+ from mms_client.types.fields import system_code
24
+ from mms_client.types.market import MarketType
25
+
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
+ class OfferStack(Payload):
56
+ """Represents a single price-quantity pair in an offer request.
57
+
58
+ Notes:
59
+ For the Day-ahead Market, the tertiary 2 sell bidding volume is mandatory.
60
+ For the Week-ahead Market, one of primary, secondary 1, secondary 2, or tertiary 1 sell bidding volumes are
61
+ mandatory.
62
+ """
63
+
64
+ # A number used to identify this PQ pair within the offer
65
+ number: int = attr(name="StackNumber", ge=1, le=20)
66
+
67
+ # The minimum quantity that must be provided before the offer can be awarded
68
+ minimum_quantity_kw: int = power_positive("MinimumQuantityInKw")
69
+
70
+ # The primary bid quantity in kW
71
+ primary_qty_kw: Optional[int] = power_positive("PrimaryOfferQuantityInKw", True)
72
+
73
+ # The first secondary bid quantity in kW
74
+ secondary_1_qty_kw: Optional[int] = power_positive("Secondary1OfferQuantityInKw", True)
75
+
76
+ # The second secondary bid quantity in kW
77
+ secondary_2_qty_kw: Optional[int] = power_positive("Secondary2OfferQuantityInKw", True)
78
+
79
+ # The first tertiary bid quantity in kW
80
+ tertiary_1_qty_kw: Optional[int] = power_positive("Tertiary1OfferQuantityInKw", True)
81
+
82
+ # The second tertiary bid quantity in kW.
83
+ tertiary_2_qty_kw: Optional[int] = power_positive("Tertiary2OfferQuantityInKw", True)
84
+
85
+ # The unit price of the power, in JPY/kW/segment
86
+ unit_price: Decimal = price("OfferUnitPrice")
87
+
88
+ # The ID of the offer to which this stack belongs
89
+ id: Optional[str] = offer_id("OfferId", True)
90
+
91
+
92
+ class OfferData(Payload):
93
+ """Describes the data common to both offer requests and responses."""
94
+
95
+ # The separate offers that make up the offer stack
96
+ stack: List[OfferStack] = element(tag="OfferStack", min_length=1, max_length=20)
97
+
98
+ # The identifier for the power resource being traded
99
+ resource: str = resource_name("ResourceName")
100
+
101
+ # Date and time of the starting block associated with the offer
102
+ start: DateTime = attr(name="StartTime")
103
+
104
+ # Date and time of the ending block associated with the offer
105
+ end: DateTime = attr(name="EndTime")
106
+
107
+ # The direction of the offer (buy, sell)
108
+ direction: Direction = attr(name="Direction")
109
+
110
+ # The type of market for which the offer is being submitted
111
+ pattern_number: Optional[int] = dr_patter_number("DrPatternNumber", True)
112
+
113
+ # The name of the BSP participant submitting the offer
114
+ bsp_participant: Optional[str] = participant("BspParticipantName", True)
115
+
116
+ # The abbreviated name of the counterparty
117
+ company_short_name: Optional[str] = company_short_name("CompanyShortName", True)
118
+
119
+ # A code identifying the TSO or MO
120
+ operator: Optional[str] = operator_code("OperatorCode", True)
121
+
122
+ # The area associated with the offer
123
+ area: Optional[AreaCode] = attr(default=None, name="Area")
124
+
125
+ # The abbreviated name of the resource being traded
126
+ resource_short_name: Optional[str] = resource_short_name("ResourceShortName", True)
127
+
128
+ # The grid code of the resource being traded
129
+ system_code: Optional[str] = system_code("SystemCode", True)
130
+
131
+ # The date and time when the offer was submitted
132
+ submission_time: Optional[DateTime] = attr(default=None, name="SubmissionTime")
133
+
134
+
135
+ class OfferCancel(Payload):
136
+ """Describes the data necessary to cancel an offer in the MMS."""
137
+
138
+ # The identifier for the power resource this offer is trading on
139
+ resource: str = resource_name("ResourceName")
140
+
141
+ # The date and time of the starting block associated with the offer
142
+ start: DateTime = attr(name="StartTime")
143
+
144
+ # The date and time of the ending block associated with the offer. You can cancel multiple blocks by specifying the
145
+ # end date and time for any block within the period from the trading day to the specified number of days.
146
+ end: DateTime = attr(name="EndTime")
147
+
148
+ # The type of market for the offer was submitted on
149
+ market_type: MarketType = attr(name="MarketType")
150
+
151
+
152
+ class OfferQuery(Payload):
153
+ """Describes the data necessary to query for offers in the MMS."""
154
+
155
+ # The type of market for the offer was submitted on
156
+ market_type: MarketType = attr(name="MarketType")
157
+
158
+ # The identifier for the power resource being requested. If this isn't provided, then all offers for the specified
159
+ # region will be returned
160
+ resource: Optional[str] = resource_name("ResourceName", True)
161
+
162
+ # The area associated with the offer. For TSOs and MOs, this field is mandatory
163
+ area: Optional[AreaCode] = attr(default=None, name="Area")
@@ -0,0 +1,130 @@
1
+ """Contains the base types necessary for communication with the MMS server."""
2
+
3
+ from enum import Enum
4
+ from typing import List
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel
8
+ from pydantic import Field
9
+
10
+
11
+ class RequestType(Enum):
12
+ """Represents the type of request to be sent to the MMS server.
13
+
14
+ Note that the first four items are valid for the Market Initiator (MI) interface, while the last item is valid for
15
+ the Other Market Initiator (OMI) interface.
16
+ """
17
+
18
+ INFO = "mp.info"
19
+ MARKET = "mp.market"
20
+ REGISTRATION = "mp.registration"
21
+ REPORT = "mp.report"
22
+ OMI = "mp.omi"
23
+
24
+
25
+ class RequestDataType(Enum):
26
+ """Represents the type of data to be sent to the MMS server.
27
+
28
+ Note that JSON is currently not supported and has been left in for future use.
29
+ """
30
+
31
+ JSON = "JSON"
32
+ XML = "XML"
33
+
34
+
35
+ class ResponseDataType(Enum):
36
+ """Represents the type of data to be received from the MMS server.
37
+
38
+ Note that JSON is currently not supported and has been left in for future use.
39
+ """
40
+
41
+ XML = "XML"
42
+ HTML = "HTML"
43
+ CSV = "CSV"
44
+ JSON = "JSON"
45
+ TXT = "TXT"
46
+
47
+
48
+ class Attachment(BaseModel):
49
+ """Represents a file attachment to be sent with a request or response."""
50
+
51
+ # The signature used to encrypt the attachment
52
+ signature: str = Field(alias="signature")
53
+
54
+ # The name of the attachment file
55
+ name: str = Field(alias="name")
56
+
57
+ # The attachment file data
58
+ data: bytes = Field(alias="binaryData")
59
+
60
+
61
+ class MmsRequest(BaseModel):
62
+ """Base class for all MMS requests."""
63
+
64
+ # The API subsystem to which the request is sent.
65
+ subsystem: RequestType = Field(alias="requestType")
66
+
67
+ # Whether the request is being made as the market operator
68
+ as_admin: bool = Field(default=False, alias="adminRole")
69
+
70
+ # Whether the request data is compressed (for future use only)
71
+ compressed: bool = Field(default=False, alias="requestDataCompressed")
72
+
73
+ # The type of data to be sent to the MMS server.
74
+ data_type: RequestDataType = Field(alias="requestDataType")
75
+
76
+ # Whether to send the request data in the response, on a successful request
77
+ respond_with_request: bool = Field(default=True, alias="sendRequestDataOnSuccess")
78
+
79
+ # Wether the response data should be compressed (for future use only)
80
+ response_compressed: bool = Field(default=False, alias="sendResponseDataCompressed")
81
+
82
+ # The signature used to encrypt the request payload
83
+ signature: str = Field(alias="requestSignature")
84
+
85
+ # The base-64 encoded payload of the request
86
+ payload: bytes = Field(alias="requestData")
87
+
88
+ # Any attached files to be sent with the request. Only 20 of these are allowed for OMI requests. For MI requests,
89
+ # the limit is 40.
90
+ attachments: List[Attachment] = Field(default=[], alias="attachmentData")
91
+
92
+ def to_arguments(self) -> dict:
93
+ """Convert the request to a dictionary of arguments for use in the MMS client."""
94
+ # First, convert the type to a dictionary format
95
+ converted = self.model_dump(by_alias=True)
96
+
97
+ # Next, convert the enum types to their string representations
98
+ converted["requestType"] = converted["requestType"].value
99
+ converted["requestDataType"] = converted["requestDataType"].value
100
+
101
+ # Finally, return the converted dictionary
102
+ return converted
103
+
104
+
105
+ class MmsResponse(BaseModel):
106
+ """Base class for all MMS responses."""
107
+
108
+ # Whether the request was successful
109
+ success: bool = Field(alias="success")
110
+
111
+ # Whether there were any warnings. This field will only be present if the request was successful.
112
+ warnings: bool = Field(default=False, alias="warnings")
113
+
114
+ # Whether the response is binary data.
115
+ is_binary: bool = Field(default=False, alias="responseBinary")
116
+
117
+ # Whether the response data is compressed (for future use only)
118
+ compressed: bool = Field(default=False, alias="responseCompressed")
119
+
120
+ # The type of data to be received from the MMS server.
121
+ data_type: ResponseDataType = Field(alias="responseDataType")
122
+
123
+ # The filename assigned to the response (for pre-generated reports)
124
+ report_filename: Optional[str] = Field(default=None, alias="responseFilename")
125
+
126
+ # The base-64 encoded payload of the response
127
+ payload: bytes = Field(alias="responseData")
128
+
129
+ # Any attached files to be sent with the response
130
+ attachments: List[Attachment] = Field(default=[], alias="attachmentData")
File without changes
@@ -0,0 +1,66 @@
1
+ """Contains error classes for the MMS client."""
2
+
3
+ from typing import Dict
4
+ from typing import List
5
+ from typing import Optional
6
+ from typing import Union
7
+
8
+ from mms_client.types.base import E
9
+ from mms_client.types.base import Messages
10
+ from mms_client.types.base import P
11
+ from mms_client.utils.web import ClientType
12
+
13
+
14
+ class AudienceError(ValueError):
15
+ """Error raised when an invalid audience is provided."""
16
+
17
+ def __init__(self, method: str, allowed: ClientType, audience: ClientType):
18
+ """Initialize the error.
19
+
20
+ Arguments:
21
+ method (str): The method that caused the error.
22
+ allowed (str): The allowed audience.
23
+ audience (str): The invalid audience.
24
+ """
25
+ self.method = method
26
+ self.message = f"{method}: Invalid client type, '{audience.name}' provided. Only '{allowed.name}' is supported."
27
+ self.allowed = allowed
28
+ self.audience = audience
29
+ super().__init__(self.message)
30
+
31
+
32
+ class MMSClientError(RuntimeError):
33
+ """Base class for MMS client errors."""
34
+
35
+ def __init__(self, method: str, message: str):
36
+ """Initialize the error.
37
+
38
+ Arguments:
39
+ method (str): The method that caused the error.
40
+ message (str): The error message.
41
+ """
42
+ super().__init__(f"{method}: {message}")
43
+ self.message = message
44
+ self.method = method
45
+
46
+
47
+ class MMSValidationError(RuntimeError):
48
+ """Error raised when a request fails validation."""
49
+
50
+ def __init__(self, method: str, envelope: E, request: Optional[Union[P, List[P]]], messages: Dict[str, Messages]):
51
+ """Initialize the validation error.
52
+
53
+ Arguments:
54
+ method (str): The method that caused the error.
55
+ message (str): The error message.
56
+ envelope (E): The request envelope.
57
+ request (P): The request data.
58
+ messages (Dict[str, Messages]): The messages returned with the payload.
59
+ """
60
+ self.message = (
61
+ f"{method}: Request of type {type(envelope).__name__} containing data {type(request).__name__} "
62
+ f"failed validation. See the logs for more information."
63
+ )
64
+ self.method = method
65
+ self.messages = messages
66
+ super().__init__(self.message)