diff --git a/setup.cfg b/setup.cfg index fb809bca..6c32d48f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,8 @@ install_requires = importlib-metadata; python_version<"3.8" aiohttp pandas - pydantic<=2.1.1 + pydantic>=1.10.8,<2.0 + [options.packages.find] where = src diff --git a/src/flexmeasures_client/s2/__init__.py b/src/flexmeasures_client/s2/__init__.py index acbe2bf5..723bc3d0 100644 --- a/src/flexmeasures_client/s2/__init__.py +++ b/src/flexmeasures_client/s2/__init__.py @@ -42,7 +42,7 @@ def wrap(*args, **kwargs): self = args[0] # TODO: implement function __hash__ in ID that returns - # the value of root, this way we would be able to use + # the value of __root__, this way we would be able to use # the ID as key directly self.incoming_messages[ get_message_id(incoming_message) @@ -63,7 +63,7 @@ def wrap(*args, **kwargs): status=ReceptionStatusValues.INVALID_DATA, ) # TODO: Discuss status - message_type = schema.model_fields.get("message_type").default + message_type = schema.__fields__.get("message_type").default setattr(wrap, "_tag", Tag(message_type)) return wrap @@ -178,7 +178,7 @@ def handle_message(self, message: pydantic.BaseModel | str | Dict) -> dict: """ if isinstance(message, pydantic.BaseModel): - message = json.loads(message.model_dump_json()) + message = json.loads(message.json()) if isinstance(message, str): message = json.loads(message) @@ -200,8 +200,10 @@ def handle_response_status(self, message: ReceptionStatus): callback_store = None # save acknowledgement status code - # TODO: implement function __hash__ in ID that returns the value of root - self.outgoing_messages_status[message.subject_message_id.root] = message.status + # TODO: implement function __hash__ in ID that returns the value of __root__ + self.outgoing_messages_status[ + message.subject_message_id.__root__ + ] = message.status # choose which callback to call, depending on the ReceptionStatus value if message.status == ReceptionStatusValues.OK: @@ -210,12 +212,12 @@ def handle_response_status(self, message: ReceptionStatus): callback_store = self.failure_callbacks # pop callback from callback_store and run it, if there exists one - if callback := callback_store.pop(message.subject_message_id.root, None): + if callback := callback_store.pop(message.subject_message_id.__root__, None): callback() # delete success callback related to this message if callback is None and (message.status != ReceptionStatusValues.OK): - self.success_callbacks.pop(message.subject_message_id.root, None) + self.success_callbacks.pop(message.subject_message_id.__root__, None) @register(RevokeObject) def handle_revoke_object(self, message: RevokeObject): @@ -223,7 +225,7 @@ def handle_revoke_object(self, message: RevokeObject): Stores the revoked object ID into the objects_revoked list """ - self.objects_revoked.append(message.object_id.root) + self.objects_revoked.append(message.object_id.__root__) return ReceptionStatus( subject_message_id=message.message_id, status=ReceptionStatusValues.OK diff --git a/src/flexmeasures_client/s2/cem.py b/src/flexmeasures_client/s2/cem.py index 056540b3..dd6645d7 100644 --- a/src/flexmeasures_client/s2/cem.py +++ b/src/flexmeasures_client/s2/cem.py @@ -105,7 +105,7 @@ async def handle_message(self, message: Dict | pydantic.BaseModel | str): response = None if isinstance(message, pydantic.BaseModel): - message = json.loads(message.model_dump_json()) + message = json.loads(message.json()) if isinstance(message, str): message = json.loads(message) @@ -158,7 +158,7 @@ async def get_message(self) -> str: # Pending for pydantic V2 to implement model.model_dump(mode="json") in # PR #1409 (https://github.com/pydantic/pydantic/issues/1409) - message = json.loads(message.model_dump_json()) + message = json.loads(message.json()) return message diff --git a/src/flexmeasures_client/s2/control_types/FRBC/__init__.py b/src/flexmeasures_client/s2/control_types/FRBC/__init__.py index 0b1fca8f..efce7c29 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/__init__.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/__init__.py @@ -59,7 +59,7 @@ def __init__(self, max_size: int = 100) -> None: def handle_system_description( self, message: FRBCSystemDescription ) -> pydantic.BaseModel: - system_description_id = message.message_id.root + system_description_id = message.message_id.__root__ # store system_description message for later self._system_description_history[system_description_id] = message @@ -77,7 +77,7 @@ async def send_actuator_status(self, status: FRBCActuatorStatus): @register(FRBCStorageStatus) def handle_storage_status(self, message: FRBCStorageStatus) -> pydantic.BaseModel: - message_id = message.message_id.root + message_id = message.message_id.__root__ self._storage_status_history[message_id] = message @@ -87,7 +87,7 @@ def handle_storage_status(self, message: FRBCStorageStatus) -> pydantic.BaseMode @register(FRBCActuatorStatus) def handle_actuator_status(self, message: FRBCActuatorStatus) -> pydantic.BaseModel: - message_id = message.message_id.root + message_id = message.message_id.__root__ self._actuator_status_history[message_id] = message @@ -129,8 +129,8 @@ async def trigger_schedule(self, system_description_id: str): instruction = FRBCInstruction( message_id=get_unique_id(), id=get_unique_id(), - actuator_id=actuator.id.root, - operation_mode=actuator.operation_modes[0].id.root, + actuator_id=actuator.id.__root__, + operation_mode=actuator.operation_modes[0].id.__root__, operation_mode_factor=0.5, execution_time=system_description.valid_from, abnormal_condition=False, diff --git a/src/flexmeasures_client/s2/control_types/FRBC/utils.py b/src/flexmeasures_client/s2/control_types/FRBC/utils.py index f6878862..e3857e78 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/utils.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/utils.py @@ -94,8 +94,8 @@ def fm_schedule_to_instructions( instruction = FRBCInstruction( message_id=get_unique_id(), id=get_unique_id(), - actuator_id=actuator.id.root, - operation_mode=operation_mode.id.root, + actuator_id=actuator.id.__root__, + operation_mode=operation_mode.id.__root__, operation_mode_factor=operation_mode_factor, execution_time=start, abnormal_condition=False, diff --git a/src/flexmeasures_client/s2/control_types/__init__.py b/src/flexmeasures_client/s2/control_types/__init__.py index 2ce1d3b0..1e0aaa7f 100644 --- a/src/flexmeasures_client/s2/control_types/__init__.py +++ b/src/flexmeasures_client/s2/control_types/__init__.py @@ -31,7 +31,7 @@ def __init__(self, max_size: int = 100) -> None: @register(InstructionStatusUpdate) def handle_instruction_status_update(self, message: InstructionStatusUpdate): - instruction_id: str = cast(str, message.instruction_id.root) + instruction_id: str = cast(str, message.instruction_id.__root__) self._instruction_status_history[instruction_id] = message.status_type diff --git a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py b/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py index 6c0c8d3a..d955128b 100644 --- a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py +++ b/src/flexmeasures_client/s2/python_s2_protocol/FRBC/messages.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import datetime -from typing import List, Literal, Optional +from typing import List, Optional -from pydantic import BaseModel, ConfigDict, Extra, Field, constr +from pydantic import BaseModel, Extra, Field, constr from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( ID, @@ -22,8 +22,11 @@ class FRBCActuatorStatus(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.ActuatorStatus"] = Field(default="FRBC.ActuatorStatus") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.ActuatorStatus", const=True) message_id: ID = Field(..., description="ID of this message") actuator_id: ID = Field( ..., description="ID of the actuator this messages refers to" @@ -46,10 +49,11 @@ class FRBCActuatorStatus(BaseModel): class FRBCFillLevelTargetProfile(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.FillLevelTargetProfile"] = Field( - default="FRBC.FillLevelTargetProfile" - ) + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.FillLevelTargetProfile", const=True) message_id: ID = Field(..., description="ID of this message") start_time: datetime = Field( ..., description="Time at which the FRBC.FillLevelTargetProfile starts." @@ -57,14 +61,17 @@ class FRBCFillLevelTargetProfile(BaseModel): elements: List[FRBCFillLevelTargetProfileElement] = Field( ..., description="List of different fill levels that have to be targeted within a given duration. There shall be at least one element. Elements must be placed in chronological order.", - max_length=288, - min_length=1, + max_items=288, + min_items=1, ) class FRBCInstruction(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.Instruction"] = Field(default="FRBC.Instruction") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.Instruction", const=True) message_id: ID = Field(..., description="ID of this message") id: ID = Field( ..., @@ -90,10 +97,11 @@ class FRBCInstruction(BaseModel): class FRBCLeakageBehaviour(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.LeakageBehaviour"] = Field( - default="FRBC.LeakageBehaviour" - ) + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.LeakageBehaviour", const=True) message_id: ID = Field(..., description="ID of this message") valid_from: datetime = Field( ..., @@ -102,14 +110,17 @@ class FRBCLeakageBehaviour(BaseModel): elements: List[FRBCLeakageBehaviourElement] = Field( ..., description="List of elements that model the leakage behaviour of the buffer. The fill_level_ranges of the elements must be contiguous.", - max_length=288, - min_length=1, + max_items=288, + min_items=1, ) class FRBCStorageStatus(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.StorageStatus"] = Field(default="FRBC.StorageStatus") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.StorageStatus", const=True) message_id: ID = Field(..., description="ID of this message") present_fill_level: float = Field( ..., description="Present fill level of the Storage" @@ -117,24 +128,28 @@ class FRBCStorageStatus(BaseModel): class FRBCSystemDescription(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.SystemDescription"] = Field( - default="FRBC.SystemDescription" - ) + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.SystemDescription", const=True) message_id: ID = Field(..., description="ID of this message") valid_from: datetime = Field( ..., description="Moment this FRBC.SystemDescription starts to be valid. If the system description is immediately valid, the DateTimeStamp should be now or in the past.", ) actuators: List[FRBCActuatorDescription] = Field( - ..., description="Details of all Actuators.", max_length=10, min_length=1 + ..., description="Details of all Actuators.", max_items=10, min_items=1 ) storage: FRBCStorageDescription = Field(..., description="Details of the storage.") class FRBCTimerStatus(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.TimerStatus"] = Field(default="FRBC.TimerStatus") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.TimerStatus", const=True) message_id: ID = Field(..., description="ID of this message") timer_id: ID = Field(..., description="The ID of the timer this message refers to") actuator_id: ID = Field( @@ -147,8 +162,11 @@ class FRBCTimerStatus(BaseModel): class FRBCUsageForecast(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["FRBC.UsageForecast"] = Field(default="FRBC.UsageForecast") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("FRBC.UsageForecast", const=True) message_id: ID = Field(..., description="ID of this message") start_time: datetime = Field( ..., description="Time at which the FRBC.UsageForecast starts." @@ -156,6 +174,6 @@ class FRBCUsageForecast(BaseModel): elements: List[FRBCUsageForecastElement] = Field( ..., description="Further elements that model the profile. There shall be at least one element. Elements must be placed in chronological order.", - max_length=288, - min_length=1, + max_items=288, + min_items=1, ) diff --git a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py b/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py index da1d596f..2b1affef 100644 --- a/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py +++ b/src/flexmeasures_client/s2/python_s2_protocol/FRBC/schemas.py @@ -4,7 +4,7 @@ from typing import List, Optional -from pydantic import BaseModel, ConfigDict, Extra, Field +from pydantic import BaseModel, Extra, Field from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( ID, @@ -18,7 +18,10 @@ class FRBCFillLevelTargetProfileElement(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + duration: Duration = Field(..., description="The duration of the element.") fill_level_range: NumberRange = Field( ..., @@ -27,7 +30,10 @@ class FRBCFillLevelTargetProfileElement(BaseModel): class FRBCLeakageBehaviourElement(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + fill_level_range: NumberRange = Field( ..., description="The fill level range for which this FRBC.LeakageBehaviourElement applies. The start of the range must be less than the end of the range.", @@ -39,7 +45,10 @@ class FRBCLeakageBehaviourElement(BaseModel): class FRBCOperationModeElement(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + fill_level_range: NumberRange = Field( ..., description="The range of the fill level for which this FRBC.OperationModeElement applies. The start of the NumberRange shall be smaller than the end of the NumberRange.", @@ -51,8 +60,8 @@ class FRBCOperationModeElement(BaseModel): power_ranges: List[PowerRange] = Field( ..., description="The power produced or consumed by this operation mode. The start of each PowerRange is associated with an operation_mode_factor of 0, the end is associated with an operation_mode_factor of 1. In the array there must be at least one PowerRange, and at most one PowerRange per CommodityQuantity.", - max_length=10, - min_length=1, + max_items=10, + min_items=1, ) running_costs: Optional[NumberRange] = Field( None, @@ -61,7 +70,10 @@ class FRBCOperationModeElement(BaseModel): class FRBCOperationMode(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + id: ID = Field( ..., description="ID of the FRBC.OperationMode. Must be unique in the scope of the FRBC.ActuatorDescription in which it is used.", @@ -73,8 +85,8 @@ class FRBCOperationMode(BaseModel): elements: List[FRBCOperationModeElement] = Field( ..., description="List of FRBC.OperationModeElements, which describe the properties of this FRBC.OperationMode depending on the fill_level. The fill_level_ranges of the items in the Array must be contiguous.", - max_length=100, - min_length=1, + max_items=100, + min_items=1, ) abnormal_condition_only: bool = Field( ..., @@ -83,7 +95,10 @@ class FRBCOperationMode(BaseModel): class FRBCActuatorDescription(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + id: ID = Field( ..., description="ID of the Actuator. Must be unique in the scope of the Resource Manager, for at least the duration of the session between Resource Manager and CEM.", @@ -93,33 +108,33 @@ class FRBCActuatorDescription(BaseModel): description="Human readable name/description for the actuator. This element is only intended for diagnostic purposes and not for HMI applications.", ) supported_commodities: List[Commodity] = Field( - ..., - description="List of all supported Commodities.", - max_length=4, - min_length=1, + ..., description="List of all supported Commodities.", max_items=4, min_items=1 ) operation_modes: List[FRBCOperationMode] = Field( ..., description="Provided FRBC.OperationModes associated with this actuator", - max_length=100, - min_length=1, + max_items=100, + min_items=1, ) transitions: List[Transition] = Field( ..., description="Possible transitions between FRBC.OperationModes associated with this actuator.", - max_length=1000, - min_length=0, + max_items=1000, + min_items=0, ) timers: List[Timer] = Field( ..., description="List of Timers associated with this actuator", - max_length=1000, - min_length=0, + max_items=1000, + min_items=0, ) class FRBCStorageDescription(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + diagnostic_label: Optional[str] = Field( None, description="Human readable name/description of the storage (e.g. hot water buffer or battery). This element is only intended for diagnostic purposes and not for HMI applications.", @@ -147,7 +162,10 @@ class FRBCStorageDescription(BaseModel): class FRBCUsageForecastElement(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + duration: Duration = Field( ..., description="Indicator for how long the given usage_rate is valid." ) diff --git a/src/flexmeasures_client/s2/python_s2_protocol/__init__.py b/src/flexmeasures_client/s2/python_s2_protocol/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py b/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py index 6a3ea045..ef99d14b 100644 --- a/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py +++ b/src/flexmeasures_client/s2/python_s2_protocol/common/messages.py @@ -3,9 +3,9 @@ from __future__ import annotations from datetime import datetime -from typing import List, Literal, Optional +from typing import List, Optional -from pydantic import BaseModel, ConfigDict, Extra, Field +from pydantic import BaseModel, Extra, Field from flexmeasures_client.s2.python_s2_protocol.common.schemas import ( ID, @@ -25,8 +25,11 @@ class Handshake(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["Handshake"] = Field(default="Handshake") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("Handshake", const=True) message_id: ID = Field(..., description="ID of this message") role: EnergyManagementRole = Field( ..., description="The role of the sender of this message" @@ -34,13 +37,16 @@ class Handshake(BaseModel): supported_protocol_versions: Optional[List[str]] = Field( None, description="Protocol versions supported by the sender of this message. This field is mandatory for the RM, but optional for the CEM.", - min_length=1, + min_items=1, ) class HandshakeResponse(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["HandshakeResponse"] = Field(default="HandshakeResponse") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("HandshakeResponse", const=True) message_id: ID = Field(..., description="ID of this message") selected_protocol_version: str = Field( ..., description="The protocol version the CEM selected for this session" @@ -48,10 +54,11 @@ class HandshakeResponse(BaseModel): class InstructionStatusUpdate(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["InstructionStatusUpdate"] = Field( - default="InstructionStatusUpdate" - ) + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("InstructionStatusUpdate", const=True) message_id: ID = Field(..., description="ID of this message") instruction_id: ID = Field( ..., description="ID of this instruction (as provided by the CEM) " @@ -65,8 +72,11 @@ class InstructionStatusUpdate(BaseModel): class PowerForecast(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["PowerForecast"] = Field(default="PowerForecast") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("PowerForecast", const=True) message_id: ID = Field(..., description="ID of this message") start_time: datetime = Field( ..., description="Start time of time period that is covered by the profile." @@ -74,14 +84,17 @@ class PowerForecast(BaseModel): elements: List[PowerForecastElement] = Field( ..., description="Elements of which this forecast consists. Contains at least one element. Elements must be placed in chronological order.", - max_length=288, - min_length=1, + max_items=288, + min_items=1, ) class PowerMeasurement(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["PowerMeasurement"] = Field(default="PowerMeasurement") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("PowerMeasurement", const=True) message_id: ID = Field(..., description="ID of this message") measurement_timestamp: datetime = Field( ..., description="Timestamp when PowerValues were measured." @@ -89,14 +102,17 @@ class PowerMeasurement(BaseModel): values: List[PowerValue] = Field( ..., description="Array of measured PowerValues. Must contain at least one item and at most one item per ‘commodity_quantity’ (defined inside the PowerValue).", # noqa: E501 - max_length=10, - min_length=1, + max_items=10, + min_items=1, ) class ReceptionStatus(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["ReceptionStatus"] = Field(default="ReceptionStatus") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("ReceptionStatus", const=True) subject_message_id: ID = Field( ..., description="The message this ReceptionStatus refers to" ) @@ -110,10 +126,11 @@ class ReceptionStatus(BaseModel): class ResourceManagerDetails(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["ResourceManagerDetails"] = Field( - default="ResourceManagerDetails" - ) + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("ResourceManagerDetails", const=True) message_id: ID = Field(..., description="ID of this message") resource_id: ID = Field( ..., @@ -123,8 +140,8 @@ class ResourceManagerDetails(BaseModel): roles: List[Role] = Field( ..., description="Each Resource Manager provides one or more energy Roles", - max_length=3, - min_length=1, + max_items=3, + min_items=1, ) manufacturer: Optional[str] = Field(None, description="Name of Manufacturer") model: Optional[str] = Field( @@ -145,8 +162,8 @@ class ResourceManagerDetails(BaseModel): available_control_types: List[ControlType] = Field( ..., description="The control types supported by this Resource Manager.", - max_length=5, - min_length=1, + max_items=5, + min_items=1, ) currency: Optional[Currency] = Field( None, @@ -159,14 +176,17 @@ class ResourceManagerDetails(BaseModel): provides_power_measurement_types: List[CommodityQuantity] = Field( ..., description="Array of all CommodityQuantities that this Resource Manager can provide measurements for. ", # noqa: E501 - max_length=10, - min_length=1, + max_items=10, + min_items=1, ) class RevokeObject(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["RevokeObject"] = Field(default="RevokeObject") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("RevokeObject", const=True) message_id: ID = Field(..., description="ID of this message") object_type: RevokableObjects = Field( ..., description="The type of object that needs to be revoked" @@ -175,8 +195,11 @@ class RevokeObject(BaseModel): class SelectControlType(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["SelectControlType"] = Field(default="SelectControlType") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("SelectControlType", const=True) message_id: ID = Field(..., description="ID of this message") control_type: ControlType = Field( ..., @@ -185,8 +208,11 @@ class SelectControlType(BaseModel): class SessionRequest(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") - message_type: Literal["SessionRequest"] = Field(default="SessionRequest") + class Config: + extra = Extra.forbid + validate_assignment = True + + message_type: str = Field("SessionRequest", const=True) message_id: ID = Field(..., description="ID of this message") request: SessionRequestType = Field(..., description="The type of request") diagnostic_label: Optional[str] = Field( diff --git a/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py b/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py index fd80049c..b0ad00bb 100644 --- a/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py +++ b/src/flexmeasures_client/s2/python_s2_protocol/common/schemas.py @@ -5,32 +5,32 @@ from enum import Enum from typing import List, Optional -from pydantic import BaseModel, ConfigDict, Extra, Field, RootModel, conint +from pydantic import BaseModel, Extra, Field, conint, constr -class ID(RootModel): +class ID(BaseModel): class Config: validate_assignment = True - root: str = Field( - ..., - description="An identifier expressed as a UUID", - title="ID", - pattern=r"[a-zA-Z0-9\-_:]{2,64}", + __root__: constr(regex=r"[a-zA-Z0-9\-_:]{2,64}") = Field( + ..., description="An identifier expressed as a UUID", title="ID" ) -class Duration(RootModel): +class Duration(BaseModel): class Config: validate_assignment = True - root: conint(ge=0) = Field( + __root__: conint(ge=0) = Field( ..., description="Duration in milliseconds", title="Duration" ) class NumberRange(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + start_of_range: float = Field( ..., description="Number that defines the start of the range" ) @@ -60,7 +60,10 @@ class CommodityQuantity(Enum): class Timer(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + id: ID = Field( ..., description="ID of the Timer. Must be unique in the scope of the OMBC.SystemDescription, FRBC.ActuatorDescription or DDBC.ActuatorDescription in which it is used.", @@ -76,7 +79,10 @@ class Timer(BaseModel): class Transition(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + id: ID = Field( ..., description="ID of the Transition. Must be unique in the scope of the OMBC.SystemDescription, FRBC.ActuatorDescription or DDBC.ActuatorDescription in which it is used.", @@ -93,14 +99,14 @@ class Transition(BaseModel): start_timers: List[ID] = Field( ..., description="List of IDs of Timers that will be (re)started when this transition is initiated", - max_length=1000, - min_length=0, + max_items=1000, + min_items=0, ) blocking_timers: List[ID] = Field( ..., description="List of IDs of Timers that block this Transition from initiating while at least one of these Timers is not yet finished", - max_length=1000, - min_length=0, + max_items=1000, + min_items=0, ) transition_costs: Optional[float] = Field( None, @@ -117,7 +123,10 @@ class Transition(BaseModel): class PowerRange(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + start_of_range: float = Field( ..., description="Power value that defines the start of the range." ) @@ -145,7 +154,10 @@ class InstructionStatus(Enum): class PowerForecastValue(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + value_upper_limit: Optional[float] = Field( None, description="The upper boundary of the range with 100\xa0% certainty the power value is in it", @@ -177,18 +189,24 @@ class PowerForecastValue(BaseModel): class PowerForecastElement(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + duration: Duration = Field(..., description="Duration of the PowerForecastElement") power_values: List[PowerForecastValue] = Field( ..., description="The values of power that are expected for the given period of time. There shall be at least one PowerForecastValue, and at most one PowerForecastValue per CommodityQuantity.", - max_length=10, - min_length=1, + max_items=10, + min_items=1, ) class PowerValue(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + commodity_quantity: CommodityQuantity = Field( ..., description="The power quantity the value refers to" ) @@ -327,7 +345,10 @@ class RoleType(Enum): class Role(BaseModel): - model_config = ConfigDict(validate_assigment=True, extra="forbid") + class Config: + extra = Extra.forbid + validate_assignment = True + role: RoleType = Field( ..., description="Role type of the Resource Manager for the given commodity" ) diff --git a/src/flexmeasures_client/s2/script/websockets_client.py b/src/flexmeasures_client/s2/script/websockets_client.py index 95643fed..cf3f0cc3 100644 --- a/src/flexmeasures_client/s2/script/websockets_client.py +++ b/src/flexmeasures_client/s2/script/websockets_client.py @@ -45,7 +45,7 @@ async def main_s2(): print("SENDING: HANDSHAKE") - await ws.send_json(message.model_dump_json()) + await ws.send_json(message.json()) response = await ws.receive() @@ -59,7 +59,7 @@ async def main_s2(): roles=[ Role(role=RoleType.ENERGY_STORAGE, commodity=Commodity.ELECTRICITY) ], - instruction_processing_delay=Duration(root=1.0), + instruction_processing_delay=Duration(__root__=1.0), available_control_types=[ ControlType.FILL_RATE_BASED_CONTROL, ControlType.NO_SELECTION, @@ -151,7 +151,7 @@ async def main_s2(): valid_from=valid_from, actuators=[actuator], storage=storage, - ).model_dump_json() + ).json() print("SENDING: FRBC.SystemDescription") diff --git a/src/flexmeasures_client/s2/utils.py b/src/flexmeasures_client/s2/utils.py index 3559a22e..29e69a61 100644 --- a/src/flexmeasures_client/s2/utils.py +++ b/src/flexmeasures_client/s2/utils.py @@ -57,9 +57,9 @@ def get_message_id(message: pydantic.BaseModel) -> str: ReceptionStatus. """ if hasattr(message, "message_id"): - return message.message_id.root + return message.message_id.__root__ elif hasattr(message, "subject_message_id"): - return message.subject_message_id.root + return message.subject_message_id.__root__ def get_reception_status( @@ -71,5 +71,5 @@ def get_reception_status( `subject_message`. By default, the status ReceptionStatusValues.OK is sent. """ return ReceptionStatus( - subject_message_id=subject_message.message_id.root, status=status + subject_message_id=subject_message.message_id.__root__, status=status ) diff --git a/tests/test_s2_coordinator.py b/tests/test_s2_coordinator.py index 0792552c..8026482d 100644 --- a/tests/test_s2_coordinator.py +++ b/tests/test_s2_coordinator.py @@ -73,7 +73,7 @@ async def test_cem(): # TODO: move into different test functions message_id=get_unique_id(), resource_id=get_unique_id(), roles=[Role(role=RoleType.ENERGY_STORAGE, commodity=Commodity.ELECTRICITY)], - instruction_processing_delay=Duration(root=1.0), + instruction_processing_delay=Duration(__root__=1.0), available_control_types=[ ControlType.FILL_RATE_BASED_CONTROL, ControlType.NO_SELECTION, @@ -170,7 +170,7 @@ async def test_cem(): # TODO: move into different test functions assert ( cem._control_types_handlers[ ControlType.FILL_RATE_BASED_CONTROL - ]._system_description_history[system_description_message.message_id.root] + ]._system_description_history[system_description_message.message_id.__root__] == system_description_message ), ( "the FRBC.SystemDescription message should be stored"