s2-python 0.4.1__py3-none-any.whl → 0.6.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.
- {s2_python-0.4.1.dist-info → s2_python-0.6.0.dist-info}/METADATA +17 -12
- s2_python-0.6.0.dist-info/RECORD +89 -0
- {s2_python-0.4.1.dist-info → s2_python-0.6.0.dist-info}/WHEEL +1 -1
- s2_python-0.6.0.dist-info/licenses/LICENSE +201 -0
- s2python/common/__init__.py +32 -0
- s2python/common/duration.py +3 -1
- s2python/common/handshake.py +2 -2
- s2python/common/handshake_response.py +2 -2
- s2python/common/instruction_status_update.py +3 -3
- s2python/common/number_range.py +5 -2
- s2python/common/power_forecast.py +3 -3
- s2python/common/power_forecast_element.py +28 -6
- s2python/common/power_forecast_value.py +1 -1
- s2python/common/power_measurement.py +27 -5
- s2python/common/power_range.py +4 -2
- s2python/common/power_value.py +1 -1
- s2python/common/reception_status.py +2 -2
- s2python/common/resource_manager_details.py +6 -6
- s2python/common/revoke_object.py +3 -3
- s2python/common/role.py +1 -1
- s2python/common/select_control_type.py +2 -2
- s2python/common/session_request.py +2 -2
- s2python/common/timer.py +3 -3
- s2python/common/transition.py +8 -8
- s2python/ddbc/__init__.py +21 -0
- s2python/ddbc/ddbc_actuator_description.py +30 -0
- s2python/ddbc/ddbc_actuator_status.py +22 -0
- s2python/ddbc/ddbc_average_demand_rate_forecast.py +28 -0
- s2python/ddbc/ddbc_average_demand_rate_forecast_element.py +21 -0
- s2python/ddbc/ddbc_instruction.py +19 -0
- s2python/ddbc/ddbc_operation_mode.py +26 -0
- s2python/ddbc/ddbc_system_description.py +29 -0
- s2python/ddbc/ddbc_timer_status.py +18 -0
- s2python/frbc/__init__.py +19 -3
- s2python/frbc/frbc_actuator_description.py +15 -9
- s2python/frbc/frbc_actuator_status.py +5 -5
- s2python/frbc/frbc_fill_level_target_profile.py +3 -3
- s2python/frbc/frbc_fill_level_target_profile_element.py +7 -6
- s2python/frbc/frbc_instruction.py +5 -5
- s2python/frbc/frbc_leakage_behaviour.py +3 -3
- s2python/frbc/frbc_leakage_behaviour_element.py +9 -6
- s2python/frbc/frbc_operation_mode.py +13 -6
- s2python/frbc/frbc_operation_mode_element.py +5 -5
- s2python/frbc/frbc_storage_description.py +2 -2
- s2python/frbc/frbc_storage_status.py +2 -2
- s2python/frbc/frbc_system_description.py +4 -4
- s2python/frbc/frbc_timer_status.py +4 -4
- s2python/frbc/frbc_usage_forecast.py +3 -3
- s2python/frbc/frbc_usage_forecast_element.py +2 -2
- s2python/generated/gen_s2.py +508 -543
- s2python/message.py +101 -6
- s2python/ombc/__init__.py +5 -0
- s2python/ombc/ombc_instruction.py +19 -0
- s2python/ombc/ombc_operation_mode.py +25 -0
- s2python/ombc/ombc_status.py +17 -0
- s2python/ombc/ombc_system_description.py +25 -0
- s2python/ombc/ombc_timer_status.py +17 -0
- s2python/pebc/__init__.py +21 -0
- s2python/pebc/pebc_allowed_limit_range.py +42 -0
- s2python/pebc/pebc_energy_constraint.py +25 -0
- s2python/pebc/pebc_instruction.py +27 -0
- s2python/pebc/pebc_power_constraints.py +77 -0
- s2python/pebc/pebc_power_envelope.py +23 -0
- s2python/pebc/pebc_power_envelope_element.py +16 -0
- s2python/ppbc/__init__.py +15 -6
- s2python/ppbc/ppbc_end_interruption_instruction.py +6 -8
- s2python/ppbc/ppbc_power_profile_definition.py +4 -6
- s2python/ppbc/ppbc_power_profile_status.py +2 -4
- s2python/ppbc/ppbc_power_sequence.py +6 -6
- s2python/ppbc/ppbc_power_sequence_container.py +5 -7
- s2python/ppbc/ppbc_power_sequence_container_status.py +7 -9
- s2python/ppbc/ppbc_power_sequence_element.py +3 -5
- s2python/ppbc/ppbc_schedule_instruction.py +6 -8
- s2python/ppbc/ppbc_start_interruption_instruction.py +6 -8
- s2python/s2_connection.py +93 -32
- s2python/s2_control_type.py +36 -0
- s2python/s2_parser.py +4 -0
- s2python/s2_validation_error.py +3 -1
- s2python/validate_values_mixin.py +29 -14
- s2_python-0.4.1.dist-info/RECORD +0 -66
- {s2_python-0.4.1.dist-info → s2_python-0.6.0.dist-info}/entry_points.txt +0 -0
- {s2_python-0.4.1.dist-info → s2_python-0.6.0.dist-info}/top_level.txt +0 -0
@@ -12,21 +12,19 @@ from s2python.validate_values_mixin import (
|
|
12
12
|
|
13
13
|
|
14
14
|
@catch_and_convert_exceptions
|
15
|
-
class PPBCPowerSequenceContainerStatus(
|
16
|
-
GenPPBCPowerSequenceContainerStatus, S2MessageComponent["PPBCPowerSequenceContainerStatus"]
|
17
|
-
):
|
15
|
+
class PPBCPowerSequenceContainerStatus(GenPPBCPowerSequenceContainerStatus, S2MessageComponent):
|
18
16
|
model_config = GenPPBCPowerSequenceContainerStatus.model_config
|
19
17
|
model_config["validate_assignment"] = True
|
20
18
|
|
21
|
-
power_profile_id: uuid.UUID = GenPPBCPowerSequenceContainerStatus.model_fields[
|
19
|
+
power_profile_id: uuid.UUID = GenPPBCPowerSequenceContainerStatus.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
22
20
|
"power_profile_id" # type: ignore[assignment]
|
23
21
|
]
|
24
|
-
sequence_container_id: uuid.UUID = GenPPBCPowerSequenceContainerStatus.model_fields[
|
22
|
+
sequence_container_id: uuid.UUID = GenPPBCPowerSequenceContainerStatus.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
25
23
|
"sequence_container_id" # type: ignore[assignment]
|
26
24
|
]
|
27
|
-
selected_sequence_id: Union[uuid.UUID, None] =
|
28
|
-
|
29
|
-
|
30
|
-
progress: Union[uuid.UUID, None] = GenPPBCPowerSequenceContainerStatus.model_fields[
|
25
|
+
selected_sequence_id: Union[uuid.UUID, None] = GenPPBCPowerSequenceContainerStatus.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
26
|
+
"selected_sequence_id"
|
27
|
+
] # type: ignore[assignment]
|
28
|
+
progress: Union[uuid.UUID, None] = GenPPBCPowerSequenceContainerStatus.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
31
29
|
"progress" # type: ignore[assignment]
|
32
30
|
]
|
@@ -13,13 +13,11 @@ from s2python.common import Duration, PowerForecastValue
|
|
13
13
|
|
14
14
|
|
15
15
|
@catch_and_convert_exceptions
|
16
|
-
class PPBCPowerSequenceElement(
|
17
|
-
GenPPBCPowerSequenceElement, S2MessageComponent["PPBCPowerSequenceElement"]
|
18
|
-
):
|
16
|
+
class PPBCPowerSequenceElement(GenPPBCPowerSequenceElement, S2MessageComponent):
|
19
17
|
model_config = GenPPBCPowerSequenceElement.model_config
|
20
18
|
model_config["validate_assignment"] = True
|
21
19
|
|
22
|
-
duration: Duration = GenPPBCPowerSequenceElement.model_fields["duration"] # type: ignore[assignment]
|
23
|
-
power_values: List[PowerForecastValue] = GenPPBCPowerSequenceElement.model_fields[
|
20
|
+
duration: Duration = GenPPBCPowerSequenceElement.model_fields["duration"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
21
|
+
power_values: List[PowerForecastValue] = GenPPBCPowerSequenceElement.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
24
22
|
"power_values"
|
25
23
|
] # type: ignore[assignment]
|
@@ -10,24 +10,22 @@ from s2python.validate_values_mixin import (
|
|
10
10
|
|
11
11
|
|
12
12
|
@catch_and_convert_exceptions
|
13
|
-
class PPBCScheduleInstruction(
|
14
|
-
GenPPBCScheduleInstruction, S2MessageComponent["PPBCScheduleInstruction"]
|
15
|
-
):
|
13
|
+
class PPBCScheduleInstruction(GenPPBCScheduleInstruction, S2MessageComponent):
|
16
14
|
model_config = GenPPBCScheduleInstruction.model_config
|
17
15
|
model_config["validate_assignment"] = True
|
18
16
|
|
19
|
-
id: uuid.UUID = GenPPBCScheduleInstruction.model_fields["id"] # type: ignore[assignment]
|
17
|
+
id: uuid.UUID = GenPPBCScheduleInstruction.model_fields["id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
20
18
|
|
21
|
-
power_profile_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields[
|
19
|
+
power_profile_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
22
20
|
"power_profile_id"
|
23
21
|
] # type: ignore[assignment]
|
24
22
|
|
25
|
-
message_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields["message_id"] # type: ignore[assignment]
|
23
|
+
message_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields["message_id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
26
24
|
|
27
|
-
sequence_container_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields[
|
25
|
+
sequence_container_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
28
26
|
"sequence_container_id"
|
29
27
|
] # type: ignore[assignment]
|
30
28
|
|
31
|
-
power_sequence_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields[
|
29
|
+
power_sequence_id: uuid.UUID = GenPPBCScheduleInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
32
30
|
"power_sequence_id"
|
33
31
|
] # type: ignore[assignment]
|
@@ -11,22 +11,20 @@ from s2python.validate_values_mixin import (
|
|
11
11
|
|
12
12
|
|
13
13
|
@catch_and_convert_exceptions
|
14
|
-
class PPBCStartInterruptionInstruction(
|
15
|
-
GenPPBCStartInterruptionInstruction, S2MessageComponent["PPBCStartInterruptionInstruction"]
|
16
|
-
):
|
14
|
+
class PPBCStartInterruptionInstruction(GenPPBCStartInterruptionInstruction, S2MessageComponent):
|
17
15
|
model_config = GenPPBCStartInterruptionInstruction.model_config
|
18
16
|
model_config["validate_assignment"] = True
|
19
17
|
|
20
|
-
id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields["id"] # type: ignore[assignment]
|
21
|
-
power_profile_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[
|
18
|
+
id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields["id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
|
19
|
+
power_profile_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
22
20
|
"power_profile_id"
|
23
21
|
] # type: ignore[assignment]
|
24
|
-
sequence_container_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[
|
22
|
+
sequence_container_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
25
23
|
"sequence_container_id"
|
26
24
|
] # type: ignore[assignment]
|
27
|
-
power_sequence_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[
|
25
|
+
power_sequence_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
28
26
|
"power_sequence_id"
|
29
27
|
] # type: ignore[assignment]
|
30
|
-
abnormal_condition: bool = GenPPBCStartInterruptionInstruction.model_fields[
|
28
|
+
abnormal_condition: bool = GenPPBCStartInterruptionInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
|
31
29
|
"abnormal_condition"
|
32
30
|
] # type: ignore[assignment]
|
s2python/s2_connection.py
CHANGED
@@ -1,14 +1,24 @@
|
|
1
|
+
try:
|
2
|
+
import websockets
|
3
|
+
except ImportError as exc:
|
4
|
+
raise ImportError(
|
5
|
+
"The 'websockets' package is required. Run 'pip install s2-python[ws]' to use this feature."
|
6
|
+
) from exc
|
7
|
+
|
1
8
|
import asyncio
|
2
9
|
import json
|
3
10
|
import logging
|
4
11
|
import time
|
5
12
|
import threading
|
6
13
|
import uuid
|
14
|
+
import ssl
|
7
15
|
from dataclasses import dataclass
|
8
|
-
from typing import Optional, List, Type, Dict, Callable, Awaitable, Union
|
16
|
+
from typing import Any, Optional, List, Type, Dict, Callable, Awaitable, Union
|
9
17
|
|
10
|
-
import
|
11
|
-
|
18
|
+
from websockets.asyncio.client import (
|
19
|
+
ClientConnection as WSConnection,
|
20
|
+
connect as ws_connect,
|
21
|
+
)
|
12
22
|
|
13
23
|
from s2python.common import (
|
14
24
|
ReceptionStatusValues,
|
@@ -35,7 +45,7 @@ logger = logging.getLogger("s2python")
|
|
35
45
|
|
36
46
|
@dataclass
|
37
47
|
class AssetDetails: # pylint: disable=too-many-instance-attributes
|
38
|
-
resource_id:
|
48
|
+
resource_id: uuid.UUID
|
39
49
|
|
40
50
|
provides_forecast: bool
|
41
51
|
provides_power_measurements: List[CommodityQuantity]
|
@@ -55,7 +65,8 @@ class AssetDetails: # pylint: disable=too-many-instance-attributes
|
|
55
65
|
) -> ResourceManagerDetails:
|
56
66
|
return ResourceManagerDetails(
|
57
67
|
available_control_types=[
|
58
|
-
control_type.get_protocol_control_type()
|
68
|
+
control_type.get_protocol_control_type()
|
69
|
+
for control_type in control_types
|
59
70
|
],
|
60
71
|
currency=self.currency,
|
61
72
|
firmware_version=self.firmware_version,
|
@@ -92,7 +103,7 @@ class SendOkay:
|
|
92
103
|
self.status_is_send.set()
|
93
104
|
|
94
105
|
await self.connection.respond_with_reception_status(
|
95
|
-
subject_message_id=
|
106
|
+
subject_message_id=self.subject_message_id,
|
96
107
|
status=ReceptionStatusValues.OK,
|
97
108
|
diagnostic_label="Processed okay.",
|
98
109
|
)
|
@@ -101,7 +112,7 @@ class SendOkay:
|
|
101
112
|
self.status_is_send.set()
|
102
113
|
|
103
114
|
self.connection.respond_with_reception_status_sync(
|
104
|
-
subject_message_id=
|
115
|
+
subject_message_id=self.subject_message_id,
|
105
116
|
status=ReceptionStatusValues.OK,
|
106
117
|
diagnostic_label="Processed okay.",
|
107
118
|
)
|
@@ -158,7 +169,7 @@ class MessageHandlers:
|
|
158
169
|
except Exception:
|
159
170
|
if not send_okay.status_is_send.is_set():
|
160
171
|
await connection.respond_with_reception_status(
|
161
|
-
subject_message_id=
|
172
|
+
subject_message_id=msg.message_id, # type: ignore[attr-defined, union-attr]
|
162
173
|
status=ReceptionStatusValues.PERMANENT_ERROR,
|
163
174
|
diagnostic_label=f"While processing message {msg.message_id} " # type: ignore[attr-defined, union-attr] # pylint: disable=line-too-long
|
164
175
|
f"an unrecoverable error occurred.",
|
@@ -170,7 +181,9 @@ class MessageHandlers:
|
|
170
181
|
type(msg),
|
171
182
|
)
|
172
183
|
|
173
|
-
def register_handler(
|
184
|
+
def register_handler(
|
185
|
+
self, msg_type: Type[S2Message], handler: S2MessageHandler
|
186
|
+
) -> None:
|
174
187
|
"""Register a coroutine function or a normal function as the handler for a specific S2 message type.
|
175
188
|
|
176
189
|
:param msg_type: The S2 message type to attach the handler to.
|
@@ -198,6 +211,8 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
198
211
|
_eventloop: asyncio.AbstractEventLoop
|
199
212
|
_stop_event: asyncio.Event
|
200
213
|
_restart_connection_event: asyncio.Event
|
214
|
+
_verify_certificate: bool
|
215
|
+
_bearer_token: Optional[str]
|
201
216
|
|
202
217
|
def __init__( # pylint: disable=too-many-arguments
|
203
218
|
self,
|
@@ -206,6 +221,8 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
206
221
|
control_types: List[S2ControlType],
|
207
222
|
asset_details: AssetDetails,
|
208
223
|
reconnect: bool = False,
|
224
|
+
verify_certificate: bool = True,
|
225
|
+
bearer_token: Optional[str] = None,
|
209
226
|
) -> None:
|
210
227
|
self.url = url
|
211
228
|
self.reconnect = reconnect
|
@@ -221,10 +238,14 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
221
238
|
self.control_types = control_types
|
222
239
|
self.role = role
|
223
240
|
self.asset_details = asset_details
|
241
|
+
self._verify_certificate = verify_certificate
|
224
242
|
|
225
|
-
self._handlers.register_handler(
|
243
|
+
self._handlers.register_handler(
|
244
|
+
SelectControlType, self.handle_select_control_type_as_rm
|
245
|
+
)
|
226
246
|
self._handlers.register_handler(Handshake, self.handle_handshake)
|
227
247
|
self._handlers.register_handler(HandshakeResponse, self.handle_handshake_response_as_rm)
|
248
|
+
self._bearer_token = bearer_token
|
228
249
|
|
229
250
|
def start_as_rm(self) -> None:
|
230
251
|
self._run_eventloop(self._run_as_rm())
|
@@ -247,8 +268,7 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
247
268
|
"""
|
248
269
|
if threading.current_thread() == self._thread:
|
249
270
|
raise RuntimeError(
|
250
|
-
"Do not call stop from the thread running the S2 connection. This results in an "
|
251
|
-
"infinite block!"
|
271
|
+
"Do not call stop from the thread running the S2 connection. This results in an infinite block!"
|
252
272
|
)
|
253
273
|
if self._eventloop.is_running():
|
254
274
|
asyncio.run_coroutine_threadsafe(self._do_stop(), self._eventloop).result()
|
@@ -304,7 +324,10 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
304
324
|
await task
|
305
325
|
except asyncio.CancelledError:
|
306
326
|
pass
|
307
|
-
except (
|
327
|
+
except (
|
328
|
+
websockets.ConnectionClosedError,
|
329
|
+
websockets.ConnectionClosedOK,
|
330
|
+
):
|
308
331
|
logger.info("The other party closed the websocket connection.")
|
309
332
|
|
310
333
|
for task in pending:
|
@@ -319,17 +342,33 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
319
342
|
|
320
343
|
async def _connect_ws(self) -> None:
|
321
344
|
try:
|
322
|
-
|
345
|
+
# set up connection arguments for SSL and bearer token, if required
|
346
|
+
connection_kwargs: Dict[str, Any] = {}
|
347
|
+
if self.url.startswith("wss://") and not self._verify_certificate:
|
348
|
+
connection_kwargs["ssl"] = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
349
|
+
connection_kwargs["ssl"].check_hostname = False
|
350
|
+
connection_kwargs["ssl"].verify_mode = ssl.CERT_NONE
|
351
|
+
|
352
|
+
if self._bearer_token:
|
353
|
+
connection_kwargs["additional_headers"] = {
|
354
|
+
"Authorization": f"Bearer {self._bearer_token}"
|
355
|
+
}
|
356
|
+
|
357
|
+
self.ws = await ws_connect(uri=self.url, **connection_kwargs)
|
323
358
|
except (EOFError, OSError) as e:
|
324
359
|
logger.info("Could not connect due to: %s", str(e))
|
325
360
|
|
326
361
|
async def _connect_as_rm(self) -> None:
|
327
362
|
await self.send_msg_and_await_reception_status_async(
|
328
363
|
Handshake(
|
329
|
-
message_id=uuid.uuid4(),
|
364
|
+
message_id=uuid.uuid4(),
|
365
|
+
role=self.role,
|
366
|
+
supported_protocol_versions=[S2_VERSION],
|
330
367
|
)
|
331
368
|
)
|
332
|
-
logger.debug(
|
369
|
+
logger.debug(
|
370
|
+
"Send handshake to CEM. Expecting Handshake and HandshakeResponse from CEM."
|
371
|
+
)
|
333
372
|
|
334
373
|
await self._handle_received_messages()
|
335
374
|
|
@@ -338,7 +377,8 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
338
377
|
) -> None:
|
339
378
|
if not isinstance(message, Handshake):
|
340
379
|
logger.error(
|
341
|
-
"Handler for Handshake received a message of the wrong type: %s",
|
380
|
+
"Handler for Handshake received a message of the wrong type: %s",
|
381
|
+
type(message),
|
342
382
|
)
|
343
383
|
return
|
344
384
|
|
@@ -361,7 +401,9 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
361
401
|
|
362
402
|
logger.debug("Received HandshakeResponse %s", message.to_json())
|
363
403
|
|
364
|
-
logger.debug(
|
404
|
+
logger.debug(
|
405
|
+
"CEM selected to use version %s", message.selected_protocol_version
|
406
|
+
)
|
365
407
|
await send_okay
|
366
408
|
logger.debug("Handshake complete. Sending first ResourceManagerDetails.")
|
367
409
|
|
@@ -381,22 +423,29 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
381
423
|
|
382
424
|
await send_okay
|
383
425
|
|
384
|
-
logger.debug(
|
426
|
+
logger.debug(
|
427
|
+
"CEM selected control type %s. Activating control type.",
|
428
|
+
message.control_type,
|
429
|
+
)
|
385
430
|
|
386
431
|
control_types_by_protocol_name = {
|
387
432
|
c.get_protocol_control_type(): c for c in self.control_types
|
388
433
|
}
|
389
|
-
selected_control_type: Optional[S2ControlType] =
|
390
|
-
message.control_type
|
434
|
+
selected_control_type: Optional[S2ControlType] = (
|
435
|
+
control_types_by_protocol_name.get(message.control_type)
|
391
436
|
)
|
392
437
|
|
393
438
|
if self._current_control_type is not None:
|
394
|
-
await self._eventloop.run_in_executor(
|
439
|
+
await self._eventloop.run_in_executor(
|
440
|
+
None, self._current_control_type.deactivate, self
|
441
|
+
)
|
395
442
|
|
396
443
|
self._current_control_type = selected_control_type
|
397
444
|
|
398
445
|
if self._current_control_type is not None:
|
399
|
-
await self._eventloop.run_in_executor(
|
446
|
+
await self._eventloop.run_in_executor(
|
447
|
+
None, self._current_control_type.activate, self
|
448
|
+
)
|
400
449
|
self._current_control_type.register_handlers(self._handlers)
|
401
450
|
|
402
451
|
async def _receive_messages(self) -> None:
|
@@ -418,7 +467,7 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
418
467
|
except json.JSONDecodeError:
|
419
468
|
await self._send_and_forget(
|
420
469
|
ReceptionStatus(
|
421
|
-
subject_message_id="00000000-0000-0000-0000-000000000000",
|
470
|
+
subject_message_id=uuid.UUID("00000000-0000-0000-0000-000000000000"),
|
422
471
|
status=ReceptionStatusValues.INVALID_DATA,
|
423
472
|
diagnostic_label="Not valid json.",
|
424
473
|
)
|
@@ -434,7 +483,7 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
434
483
|
)
|
435
484
|
else:
|
436
485
|
await self.respond_with_reception_status(
|
437
|
-
subject_message_id="00000000-0000-0000-0000-000000000000",
|
486
|
+
subject_message_id=uuid.UUID("00000000-0000-0000-0000-000000000000"),
|
438
487
|
status=ReceptionStatusValues.INVALID_DATA,
|
439
488
|
diagnostic_label="Message appears valid json but could not find a message_id field.",
|
440
489
|
)
|
@@ -465,9 +514,11 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
465
514
|
self._restart_connection_event.set()
|
466
515
|
|
467
516
|
async def respond_with_reception_status(
|
468
|
-
self, subject_message_id:
|
517
|
+
self, subject_message_id: uuid.UUID, status: ReceptionStatusValues, diagnostic_label: str
|
469
518
|
) -> None:
|
470
|
-
logger.debug(
|
519
|
+
logger.debug(
|
520
|
+
"Responding to message %s with status %s", subject_message_id, status
|
521
|
+
)
|
471
522
|
await self._send_and_forget(
|
472
523
|
ReceptionStatus(
|
473
524
|
subject_message_id=subject_message_id,
|
@@ -477,15 +528,20 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
477
528
|
)
|
478
529
|
|
479
530
|
def respond_with_reception_status_sync(
|
480
|
-
self, subject_message_id:
|
531
|
+
self, subject_message_id: uuid.UUID, status: ReceptionStatusValues, diagnostic_label: str
|
481
532
|
) -> None:
|
482
533
|
asyncio.run_coroutine_threadsafe(
|
483
|
-
self.respond_with_reception_status(
|
534
|
+
self.respond_with_reception_status(
|
535
|
+
subject_message_id, status, diagnostic_label
|
536
|
+
),
|
484
537
|
self._eventloop,
|
485
538
|
).result()
|
486
539
|
|
487
540
|
async def send_msg_and_await_reception_status_async(
|
488
|
-
self,
|
541
|
+
self,
|
542
|
+
s2_msg: S2Message,
|
543
|
+
timeout_reception_status: float = 5.0,
|
544
|
+
raise_on_error: bool = True,
|
489
545
|
) -> ReceptionStatus:
|
490
546
|
await self._send_and_forget(s2_msg)
|
491
547
|
logger.debug(
|
@@ -506,12 +562,17 @@ class S2Connection: # pylint: disable=too-many-instance-attributes
|
|
506
562
|
raise
|
507
563
|
|
508
564
|
if reception_status.status != ReceptionStatusValues.OK and raise_on_error:
|
509
|
-
raise RuntimeError(
|
565
|
+
raise RuntimeError(
|
566
|
+
f"ReceptionStatus was not OK but rather {reception_status.status}"
|
567
|
+
)
|
510
568
|
|
511
569
|
return reception_status
|
512
570
|
|
513
571
|
def send_msg_and_await_reception_status_sync(
|
514
|
-
self,
|
572
|
+
self,
|
573
|
+
s2_msg: S2Message,
|
574
|
+
timeout_reception_status: float = 5.0,
|
575
|
+
raise_on_error: bool = True,
|
515
576
|
) -> ReceptionStatus:
|
516
577
|
return asyncio.run_coroutine_threadsafe(
|
517
578
|
self.send_msg_and_await_reception_status_async(
|
s2python/s2_control_type.py
CHANGED
@@ -4,6 +4,7 @@ import typing
|
|
4
4
|
from s2python.common import ControlType as ProtocolControlType
|
5
5
|
from s2python.frbc import FRBCInstruction
|
6
6
|
from s2python.ppbc import PPBCScheduleInstruction
|
7
|
+
from s2python.ombc import OMBCInstruction
|
7
8
|
from s2python.message import S2Message
|
8
9
|
|
9
10
|
if typing.TYPE_CHECKING:
|
@@ -66,6 +67,41 @@ class PPBCControlType(S2ControlType):
|
|
66
67
|
"""Overwrite with the actual deactivation logic of your Resource Manager for this particular control type."""
|
67
68
|
|
68
69
|
|
70
|
+
class OMBCControlType(S2ControlType):
|
71
|
+
def get_protocol_control_type(self) -> ProtocolControlType:
|
72
|
+
return ProtocolControlType.OPERATION_MODE_BASED_CONTROL
|
73
|
+
|
74
|
+
def register_handlers(self, handlers: "MessageHandlers") -> None:
|
75
|
+
handlers.register_handler(OMBCInstruction, self.handle_instruction)
|
76
|
+
|
77
|
+
@abc.abstractmethod
|
78
|
+
def handle_instruction(
|
79
|
+
self, conn: "S2Connection", msg: S2Message, send_okay: typing.Callable[[], None]
|
80
|
+
) -> None: ...
|
81
|
+
|
82
|
+
@abc.abstractmethod
|
83
|
+
def activate(self, conn: "S2Connection") -> None:
|
84
|
+
"""Overwrite with the actual dctivation logic of your Resource Manager for this particular control type."""
|
85
|
+
|
86
|
+
@abc.abstractmethod
|
87
|
+
def deactivate(self, conn: "S2Connection") -> None:
|
88
|
+
"""Overwrite with the actual deactivation logic of your Resource Manager for this particular control type."""
|
89
|
+
|
90
|
+
|
91
|
+
class PEBCControlType(S2ControlType):
|
92
|
+
def get_protocol_control_type(self) -> ProtocolControlType:
|
93
|
+
return ProtocolControlType.POWER_ENVELOPE_BASED_CONTROL
|
94
|
+
|
95
|
+
def register_handlers(self, handlers: "MessageHandlers") -> None:
|
96
|
+
pass
|
97
|
+
|
98
|
+
@abc.abstractmethod
|
99
|
+
def activate(self, conn: "S2Connection") -> None: ...
|
100
|
+
|
101
|
+
@abc.abstractmethod
|
102
|
+
def deactivate(self, conn: "S2Connection") -> None: ...
|
103
|
+
|
104
|
+
|
69
105
|
class NoControlControlType(S2ControlType):
|
70
106
|
def get_protocol_control_type(self) -> ProtocolControlType:
|
71
107
|
return ProtocolControlType.NOT_CONTROLABLE
|
s2python/s2_parser.py
CHANGED
@@ -24,6 +24,7 @@ from s2python.frbc import (
|
|
24
24
|
FRBCTimerStatus,
|
25
25
|
FRBCUsageForecast,
|
26
26
|
)
|
27
|
+
from s2python.pebc import PEBCPowerConstraints, PEBCEnergyConstraint, PEBCInstruction
|
27
28
|
from s2python.ppbc import PPBCScheduleInstruction
|
28
29
|
|
29
30
|
from s2python.message import S2Message
|
@@ -48,6 +49,9 @@ TYPE_TO_MESSAGE_CLASS: Dict[str, Type[S2Message]] = {
|
|
48
49
|
"FRBC.TimerStatus": FRBCTimerStatus,
|
49
50
|
"FRBC.UsageForecast": FRBCUsageForecast,
|
50
51
|
"PPBC.ScheduleInstruction": PPBCScheduleInstruction,
|
52
|
+
"PEBC.PowerConstraints": PEBCPowerConstraints,
|
53
|
+
"PEBC.Instruction": PEBCInstruction,
|
54
|
+
"PEBC.EnergyConstraint": PEBCEnergyConstraint,
|
51
55
|
"Handshake": Handshake,
|
52
56
|
"HandshakeResponse": HandshakeResponse,
|
53
57
|
"InstructionStatusUpdate": InstructionStatusUpdate,
|
s2python/s2_validation_error.py
CHANGED
@@ -10,4 +10,6 @@ class S2ValidationError(Exception):
|
|
10
10
|
class_: Optional[Type]
|
11
11
|
obj: object
|
12
12
|
msg: str
|
13
|
-
pydantic_validation_error: Union[
|
13
|
+
pydantic_validation_error: Union[
|
14
|
+
ValidationErrorV1, ValidationError, TypeError, None
|
15
|
+
]
|
@@ -1,22 +1,34 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import (
|
2
|
+
TypeVar,
|
3
|
+
Type,
|
4
|
+
Callable,
|
5
|
+
Any,
|
6
|
+
Union,
|
7
|
+
AbstractSet,
|
8
|
+
Mapping,
|
9
|
+
List,
|
10
|
+
Dict,
|
11
|
+
)
|
12
|
+
|
13
|
+
from typing_extensions import Self
|
2
14
|
|
3
|
-
from pydantic import BaseModel, ValidationError # pylint: disable=no-name-in-module
|
4
15
|
from pydantic.v1.error_wrappers import display_errors # pylint: disable=no-name-in-module
|
5
16
|
|
17
|
+
from pydantic import ( # pylint: disable=no-name-in-module
|
18
|
+
BaseModel,
|
19
|
+
ValidationError,
|
20
|
+
)
|
21
|
+
|
6
22
|
from s2python.s2_validation_error import S2ValidationError
|
7
23
|
|
8
|
-
B_co = TypeVar("B_co", bound=BaseModel, covariant=True)
|
9
24
|
|
10
25
|
IntStr = Union[int, str]
|
11
26
|
AbstractSetIntStr = AbstractSet[IntStr]
|
12
27
|
MappingIntStrAny = Mapping[IntStr, Any]
|
13
28
|
|
14
29
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
class S2MessageComponent(BaseModel, Generic[C]):
|
19
|
-
def to_json(self: C) -> str:
|
30
|
+
class S2MessageComponent(BaseModel):
|
31
|
+
def to_json(self) -> str:
|
20
32
|
try:
|
21
33
|
return self.model_dump_json(by_alias=True, exclude_none=True)
|
22
34
|
except (ValidationError, TypeError) as e:
|
@@ -24,17 +36,17 @@ class S2MessageComponent(BaseModel, Generic[C]):
|
|
24
36
|
type(self), self, "Pydantic raised a format validation error.", e
|
25
37
|
) from e
|
26
38
|
|
27
|
-
def to_dict(self
|
39
|
+
def to_dict(self) -> Dict[str, Any]:
|
28
40
|
return self.model_dump()
|
29
41
|
|
30
42
|
@classmethod
|
31
|
-
def from_json(cls
|
32
|
-
gen_model
|
43
|
+
def from_json(cls, json_str: str) -> Self:
|
44
|
+
gen_model = cls.model_validate_json(json_str)
|
33
45
|
return gen_model
|
34
46
|
|
35
47
|
@classmethod
|
36
|
-
def from_dict(cls:
|
37
|
-
gen_model
|
48
|
+
def from_dict(cls, json_dict: Dict[str, Any]) -> Self:
|
49
|
+
gen_model = cls.model_validate(json_dict)
|
38
50
|
return gen_model
|
39
51
|
|
40
52
|
|
@@ -59,7 +71,10 @@ def convert_to_s2exception(f: Callable) -> Callable:
|
|
59
71
|
return inner
|
60
72
|
|
61
73
|
|
62
|
-
|
74
|
+
S = TypeVar("S", bound=S2MessageComponent)
|
75
|
+
|
76
|
+
|
77
|
+
def catch_and_convert_exceptions(input_class: Type[S]) -> Type[S]:
|
63
78
|
input_class.__init__ = convert_to_s2exception(input_class.__init__) # type: ignore[method-assign]
|
64
79
|
input_class.__setattr__ = convert_to_s2exception(input_class.__setattr__) # type: ignore[method-assign]
|
65
80
|
input_class.model_validate_json = convert_to_s2exception( # type: ignore[method-assign]
|
s2_python-0.4.1.dist-info/RECORD
DELETED
@@ -1,66 +0,0 @@
|
|
1
|
-
s2python/__init__.py,sha256=e5lwvqsPl-z7IfEd0hRQhLBRKBYcuw2eqrecXnMfLdg,384
|
2
|
-
s2python/message.py,sha256=Id-CleYk6ClVh3o5meVtRECLNwQHlyddNSOq0-d2bZk,1027
|
3
|
-
s2python/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
-
s2python/reception_status_awaiter.py,sha256=jKMliFk1XxwsEGtx3vFESbJhUtClB7cTu-td90-qBN8,2137
|
5
|
-
s2python/s2_connection.py,sha256=CTTTGzJ6IG9PcQpC5XtnThaYQRpp66gQrauPK8KygOE,20166
|
6
|
-
s2python/s2_control_type.py,sha256=4L6o_QG9ilYA8c3QWpSjc77xwxEHHsAGZO8WQDgxDr0,2860
|
7
|
-
s2python/s2_parser.py,sha256=x2JSCSEvXMCt-0kPDH4zER--CsyFYyRAaezr76V_8Qc,4354
|
8
|
-
s2python/s2_validation_error.py,sha256=BkOLoNsrcQ3MzdCYaPDgs1Wu6lPdlQDpZsTpykKGQmE,384
|
9
|
-
s2python/utils.py,sha256=QX9b-mi-H_YUGTmGmJsrAbaWWM3dgaoaRLRXHHlaZDE,212
|
10
|
-
s2python/validate_values_mixin.py,sha256=3WbauPprzWpGdyDUYPoELICi2NbHrCFzlQ7AtrpDSis,2568
|
11
|
-
s2python/version.py,sha256=IBzoytgbYYYekQnSTfSmWeYAZ4c_yUFU2oLIAG4UYjs,45
|
12
|
-
s2python/common/__init__.py,sha256=yEAXCS59XpNOEXbEXoN04SwultP-3evgVzFuEUSgB5I,1345
|
13
|
-
s2python/common/duration.py,sha256=5_zi0wv98dr8lHXh2RdHATzW4Un8ZNUx4-zaI1t77Ak,667
|
14
|
-
s2python/common/handshake.py,sha256=bsx64flKxVUUMT18O47pV1pnnpXcAGXMJbRi9q4GjwU,467
|
15
|
-
s2python/common/handshake_response.py,sha256=5cAbtLTZ8fkhUw-XdMlGBro3XKWqXXLw3YUavvwLYNg,523
|
16
|
-
s2python/common/instruction_status_update.py,sha256=lo5HxoEGFp8bU0-AQ7bNxuDj1StZ73GE_2sGDA7o5vI,692
|
17
|
-
s2python/common/number_range.py,sha256=N0OUoGtkdYg4GDUwvVHKLjdXxcy9mijgwqmmA079P6k,741
|
18
|
-
s2python/common/power_forecast.py,sha256=C0D6XOphdcEVjfb0py-l2zLnNC-udWmlDD7g2Hre2uo,704
|
19
|
-
s2python/common/power_forecast_element.py,sha256=M3weDKlBASjS_VHt4XKdms2fpxEZy17kWuLTUXsFibU,805
|
20
|
-
s2python/common/power_forecast_value.py,sha256=im5kLyTfDjLuODFWoIuVgVxKjX2FpyPXYoxonjuOpu0,411
|
21
|
-
s2python/common/power_measurement.py,sha256=mMVpe3T86Xu6ctA2Rpc50aB4WS7xuLXgGVSbB4lykHQ,693
|
22
|
-
s2python/common/power_range.py,sha256=FKhJRkT3dzUCeK8P3mrcRVtJHPf2WH-eQDottvEMpBs,686
|
23
|
-
s2python/common/power_value.py,sha256=ujo0yxHnAd-LCIQQuIpNU01jv611UCbcXKl8ilQIILY,363
|
24
|
-
s2python/common/reception_status.py,sha256=HXdaTu5B2iuHb1pg8d5TMprfi80rUmCgCxrrDFLJB0w,525
|
25
|
-
s2python/common/resource_manager_details.py,sha256=QTW6VTn-Y_fvVKX6qGgDMuvry5twmDqHjRvOZXgkuLI,1044
|
26
|
-
s2python/common/revoke_object.py,sha256=7OVNuwu5aMfZ3GJ_b_SRNoTxiX80FY9obp7JHIySzaY,585
|
27
|
-
s2python/common/role.py,sha256=eHKwnie_7eK8k1CNA7S0TbwmkQR84Pzkwdk8aUaUldw,327
|
28
|
-
s2python/common/select_control_type.py,sha256=Rbvem30LrrPYTSjwspc-PJyxoNScv9XRVXJ58-WRotk,523
|
29
|
-
s2python/common/session_request.py,sha256=ztbvGjMwiEmdJ_ToFuPV9Lj_IHJ78fP7IsCt8rMvMu8,502
|
30
|
-
s2python/common/support.py,sha256=Kbrf_KGB45Wfr8j2pqDe1lLde6CIr3nl_LYkWnilmV0,1015
|
31
|
-
s2python/common/timer.py,sha256=PWJjHXfL6yr2VQ8O2HwtYB0o3Jw4TdP2iMX2Wdp7Ic4,556
|
32
|
-
s2python/common/transition.py,sha256=2v0fkxudPhVqmnd-WajJhBsYRnJLAqJU9z1eqQJy8zw,1064
|
33
|
-
s2python/frbc/__init__.py,sha256=ROV3qZoldPkdgVFfMQr5Mf3GDfBzXaMfhNNCuXY6T0s,1104
|
34
|
-
s2python/frbc/frbc_actuator_description.py,sha256=fsgYdKvxZnUCuUuyoN0Ibu5FoA0vUBSrKy6WQJGBfGk,6166
|
35
|
-
s2python/frbc/frbc_actuator_status.py,sha256=cPyAz0B0rThfueKr3CWx61E3eJ8Lp29a-nJPyTPpbls,973
|
36
|
-
s2python/frbc/frbc_fill_level_target_profile.py,sha256=suRBgT8b9HQjfafW-sjqzjFpgJ2vOf73nmL5-Q6WhOo,880
|
37
|
-
s2python/frbc/frbc_fill_level_target_profile_element.py,sha256=V7O7oDDfifLcJsSXYWEd00X3C2TPoU8TcrRu2yLWVVU,1280
|
38
|
-
s2python/frbc/frbc_instruction.py,sha256=Q8w9KB4x6mGsLR-3ADzsyWWf0-uTWGZ31rnKGC9TkIc,809
|
39
|
-
s2python/frbc/frbc_leakage_behaviour.py,sha256=E23McMu4zbjDIUR_MNBlyUuh1chGM-EKTMuiHqjV_wg,794
|
40
|
-
s2python/frbc/frbc_leakage_behaviour_element.py,sha256=B3Tzlu3iMVV3NA1WJds_LDl3T_lBGL3HjGiEin02_pQ,1103
|
41
|
-
s2python/frbc/frbc_operation_mode.py,sha256=w0--55ZByWQukbotR6VpoL9GvNbeNyHcs3NqHAzjq6w,1899
|
42
|
-
s2python/frbc/frbc_operation_mode_element.py,sha256=64_FJzfCRoUgRCdfA1HpLO9lTE1WBxL3Pbn0vaNeX8c,1073
|
43
|
-
s2python/frbc/frbc_storage_description.py,sha256=Jc0zpAwf3E7Grl6v0fkSa4yA8-2-klO1aY-50l-u56E,622
|
44
|
-
s2python/frbc/frbc_storage_status.py,sha256=h6cHTb7Msw5TckPzKsiI3KTSa7Hr5BcXIelF8gmFIdw,523
|
45
|
-
s2python/frbc/frbc_system_description.py,sha256=_scCAVe4sHEhQMHkDvD2pKGnGLzdnm4uARbdcw0inKk,980
|
46
|
-
s2python/frbc/frbc_timer_status.py,sha256=sRsvXxsT_TDlPiacLkZlCZM3y36NZRGqj6RKJeil-yc,711
|
47
|
-
s2python/frbc/frbc_usage_forecast.py,sha256=MWY4Nr2T9NGjQhf7rxTtkqnuSdApTjro81-o_buLABk,747
|
48
|
-
s2python/frbc/frbc_usage_forecast_element.py,sha256=wQJCBoYFt2HXNaKNL3vK_FpTx5wXvRH03NOw_JWOal8,601
|
49
|
-
s2python/frbc/rm.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
50
|
-
s2python/generated/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
51
|
-
s2python/generated/gen_s2.py,sha256=eg9J0pcWxCy0oXO8eq9AtoB-GONHZLNd8JANpMhKQCQ,63573
|
52
|
-
s2python/ppbc/__init__.py,sha256=Rnfl_1n0gkc9mASSzBBMFEKYzJqugV5POtttjLWx_pI,665
|
53
|
-
s2python/ppbc/ppbc_end_interruption_instruction.py,sha256=0ZXQATN8L35xm9-QWCU8ftnU1_Na9Oq3HzSYna_SMcU,1197
|
54
|
-
s2python/ppbc/ppbc_power_profile_definition.py,sha256=3y5lw7eeDVVa6JecoGlZrqr6bcQRaDoLO9EISPle3ks,991
|
55
|
-
s2python/ppbc/ppbc_power_profile_status.py,sha256=E-mNhdaPiuB6qFsG_F1ftIlwaxF75972nvIHKSpau5w,767
|
56
|
-
s2python/ppbc/ppbc_power_sequence.py,sha256=DCeNstEAUvwCiN1bKDd85gE6pgiataJbwGui6pEk5_M,1158
|
57
|
-
s2python/ppbc/ppbc_power_sequence_container.py,sha256=L0kD7zkJj9OBafzzA6zcUgFnn5wCx6VzlGoLX3GDf9U,829
|
58
|
-
s2python/ppbc/ppbc_power_sequence_container_status.py,sha256=3Wq3NasIh_mLDEZvnNsgB8AY8CSd4qQKpPJxXwS7ZiA,1160
|
59
|
-
s2python/ppbc/ppbc_power_sequence_element.py,sha256=sQMJlIRhMhj_crpaRFDWhbP4eCCw4haFq5sD-ptQsS0,797
|
60
|
-
s2python/ppbc/ppbc_schedule_instruction.py,sha256=yHDO5omwKYV_P_v70D4gZ26K5MEIFFboX1sp3J6_atA,1098
|
61
|
-
s2python/ppbc/ppbc_start_interruption_instruction.py,sha256=6Atu6E-Hfs8S2KIdHdGJd9F5gRFqZLNb-dpnphj8hNU,1219
|
62
|
-
s2_python-0.4.1.dist-info/METADATA,sha256=CJILWeqNUp0VSPUdlLafySNPJag6SvoeYAPO9b5_cQs,3595
|
63
|
-
s2_python-0.4.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
64
|
-
s2_python-0.4.1.dist-info/entry_points.txt,sha256=feX-xmgJZgSe5-jxMgFKPKCJz4Ys3eQcGrsXsirNZyM,61
|
65
|
-
s2_python-0.4.1.dist-info/top_level.txt,sha256=OLFq0oDhr77Mp-EYLEcWk5P3jvooOt4IHkTI5KYJMc8,9
|
66
|
-
s2_python-0.4.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|