Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.8
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile --extra=development --extra=docs --extra=testing --extra=ws --output-file=./dev-requirements.txt pyproject.toml
Expand Down Expand Up @@ -65,6 +65,7 @@ imagesize==1.4.1
importlib-metadata==8.5.0
# via
# build
# setuptools-scm
# sphinx
inflect==5.6.2
# via datamodel-code-generator
Expand Down Expand Up @@ -99,6 +100,7 @@ packaging==24.2
# datamodel-code-generator
# pyproject-api
# pytest
# setuptools-scm
# sphinx
# tox
pathspec==0.12.1
Expand Down Expand Up @@ -151,15 +153,15 @@ pytest-coverage==0.0
pytest-timer==1.0.0
# via s2-python (pyproject.toml)
pytz==2025.1
# via
# babel
# s2-python (pyproject.toml)
# via s2-python (pyproject.toml)
pyyaml==6.0.2
# via
# datamodel-code-generator
# pre-commit
requests==2.32.3
# via sphinx
setuptools-scm==8.3.1
# via s2-python (pyproject.toml)
six==1.17.0
# via sphinxcontrib-httpdomain
snowballstemmer==2.2.0
Expand Down Expand Up @@ -208,6 +210,7 @@ tomli==2.2.1
# pylint
# pyproject-api
# pytest
# setuptools-scm
# tox
tomlkit==0.13.2
# via pylint
Expand All @@ -217,14 +220,15 @@ types-pytz==2024.2.0.20241221
# via s2-python (pyproject.toml)
typing-extensions==4.12.2
# via
# annotated-types
# astroid
# black
# mypy
# pydantic
# pydantic-core
# pylint
# pyright
# s2-python (pyproject.toml)
# setuptools-scm
# tox
urllib3==2.2.3
# via requests
Expand All @@ -238,7 +242,7 @@ wheel==0.45.1
# via pip-tools
zipp==3.20.2
# via importlib-metadata
setuptools-scm==8.3.1

# The following packages are considered to be unsafe in a requirements file:
# pip
# setuptools
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies = [
"pydantic>=2.8.2",
"pytz",
"click",
"typing-extensions",
]
classifiers = [
"Development Status :: 4 - Beta",
Expand Down Expand Up @@ -52,6 +53,7 @@ development = [
"datamodel-code-generator",
"pre-commit",
"tox",
"setuptools_scm",
]
docs = [
"sphinx",
Expand Down
8 changes: 7 additions & 1 deletion src/s2python/message.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from typing import Union

from typing_extensions import TypeAlias

from s2python.frbc import (
FRBCActuatorDescription,
FRBCActuatorStatus,
Expand Down Expand Up @@ -76,7 +78,7 @@
Transition,
)

S2Message = Union[
S2MessageWithID: TypeAlias = Union[
DDBCAverageDemandRateForecast,
DDBCInstruction,
DDBCSystemDescription,
Expand Down Expand Up @@ -111,6 +113,10 @@
InstructionStatusUpdate,
PowerForecast,
PowerMeasurement,
]

S2Message: TypeAlias = Union[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this fix the issues we're having with subclassing and the message_id?
Do we have a testcase for it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MauriceHendrix What is the issue you have with subclassing? This allows us to refer to S2 messages which have a message id with S2MessageWithID and all S2 messages (even those without a message id) with S2Message. However, S2MessageWithID is currently not a type that is a result from any function in this library so it is up to the user to actually differentiate somewhere in their code if the message they are currently refering to is one with a message id or if it is unknown. It is a bit hard to test given it is a typing alias, but I provided some usage here: https://github.com/flexiblepower/s2-python/pull/134/files#diff-2149287ebe9503504f407458b92eea4f7e4bb7cffa11e936cab5730f40bf9a13R72

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it I think this was more of a problem in our Java code (which we did solve a while ago)

S2MessageWithID,
ReceptionStatus,
]

Expand Down
1 change: 1 addition & 0 deletions src/s2python/ppbc/ppbc_end_interruption_instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class PPBCEndInterruptionInstruction(GenPPBCEndInterruptionInstruction, S2Messag
model_config["validate_assignment"] = True

id: uuid.UUID = GenPPBCEndInterruptionInstruction.model_fields["id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
message_id: uuid.UUID = GenPPBCEndInterruptionInstruction.model_fields["message_id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
power_profile_id: uuid.UUID = GenPPBCEndInterruptionInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
"power_profile_id"
] # type: ignore[assignment]
Expand Down
2 changes: 2 additions & 0 deletions src/s2python/ppbc/ppbc_power_profile_status.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import List
import uuid

from s2python.generated.gen_s2 import (
PPBCPowerProfileStatus as GenPPBCPowerProfileStatus,
Expand All @@ -19,6 +20,7 @@ class PPBCPowerProfileStatus(GenPPBCPowerProfileStatus, S2MessageComponent):
model_config = GenPPBCPowerProfileStatus.model_config
model_config["validate_assignment"] = True

message_id: uuid.UUID = GenPPBCPowerProfileStatus.model_fields["message_id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
sequence_container_status: List[PPBCPowerSequenceContainerStatus] = ( # type: ignore[reportIncompatibleVariableOverride]
GenPPBCPowerProfileStatus.model_fields["sequence_container_status"] # type: ignore[assignment]
)
1 change: 1 addition & 0 deletions src/s2python/ppbc/ppbc_start_interruption_instruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class PPBCStartInterruptionInstruction(GenPPBCStartInterruptionInstruction, S2Me
model_config["validate_assignment"] = True

id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields["id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
message_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields["message_id"] # type: ignore[assignment,reportIncompatibleVariableOverride]
power_profile_id: uuid.UUID = GenPPBCStartInterruptionInstruction.model_fields[ # type: ignore[reportIncompatibleVariableOverride]
"power_profile_id"
] # type: ignore[assignment]
Expand Down
23 changes: 22 additions & 1 deletion src/s2python/validate_values_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ def __setattr__(self, name: str, value: Any) -> None:
) from e

def to_json(self) -> str:
"""Convert the S2 message or message component to a json string.

:return: The json string.
"""
try:
return self.model_dump_json(by_alias=True, exclude_none=True)
except (ValidationError, TypeError) as e:
Expand All @@ -43,7 +47,24 @@ def to_json(self) -> str:
) from e

def to_dict(self) -> Dict[str, Any]:
return self.model_dump()
"""Convert the S2 message or message component to a Python dictionary that contains Python-native structures..

Conversion happens according to https://docs.pydantic.dev/latest/concepts/conversion_table/#__tabbed_1_4
in non-strict 'python' mode.

:return: A dictionary with python datastructures.
"""
return self.model_dump(mode='python')

def to_json_dict(self) -> Dict[str, Any]:
"""Convert the S2 message or message component to a Python dictionary which is json serializable.

Conversion happens according to https://docs.pydantic.dev/latest/concepts/conversion_table/#__tabbed_1_2 in
non-strict 'json' mode.

:return: A dictionary with json-serializable values.
"""
return self.model_dump(mode='json')

@classmethod
def from_json(cls, json_str: str) -> Self:
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/message_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import importlib
import inspect
import pkgutil
import uuid
from typing import get_args

from s2python import message
from s2python.common import ReceptionStatus, ReceptionStatusValues
from s2python.frbc import FRBCStorageStatus
from s2python.validate_values_mixin import S2MessageComponent


Expand Down Expand Up @@ -61,5 +64,24 @@ def test_import_s2_messages__pebc(self):

def test_import_s2_messages__ppbc(self):
self._test_import_s2_messages("s2python.ppbc")

def test_import_s2_messages__ombc(self):
self._test_import_s2_messages("s2python.ombc")


def test__s2messagewithid__type_has_message_id_field(self):
# Arrange
message_with_id: message.S2MessageWithID = FRBCStorageStatus(message_id=uuid.uuid4(),
present_fill_level=4.0)

# Act / Assert
self.assertIsInstance(message_with_id.message_id, uuid.UUID)

def test__s2message__type_has_no_message_id_field(self):
# Arrange
message_without_id: message.S2Message = ReceptionStatus(subject_message_id=uuid.uuid4(),
status=ReceptionStatusValues.OK,
diagnostic_label='Hello!')

# Act / Assert
self.assertFalse(hasattr(message_without_id, 'message_id'))
68 changes: 68 additions & 0 deletions tests/unit/validate_values_mixing_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import json
import unittest
import datetime
import uuid

from s2python.validate_values_mixin import S2MessageComponent


class MockS2Message(S2MessageComponent):
some_uuid: uuid.UUID
some_str: str
some_float: float
some_datetime: datetime.datetime
some_timedelta: datetime.timedelta


def example_message() -> MockS2Message:
return MockS2Message(some_uuid=uuid.uuid4(),
some_str='asdaa',
some_float=3.14,
some_datetime=datetime.datetime.now(),
some_timedelta=datetime.timedelta(hours=1, minutes=1))


class TestS2MessageComponent(unittest.TestCase):
def test__to_json__okay(self):
# Arrange
message = example_message()

# Act
json_str = message.to_json()

# Assert
message_json = json.loads(json_str)
self.assertEqual(message_json['some_uuid'], str(message.some_uuid))
self.assertEqual(message_json['some_str'], message.some_str)
self.assertEqual(message_json['some_float'], message.some_float)
self.assertEqual(message_json['some_datetime'], message.some_datetime.isoformat())
self.assertEqual(message_json['some_timedelta'], 'PT1H1M')

def test__to_dict__okay(self):
# Arrange
message = example_message()

# Act
message_dict = message.to_dict()

# Assert
self.assertEqual(message_dict['some_uuid'], message.some_uuid)
self.assertEqual(message_dict['some_str'], message.some_str)
self.assertEqual(message_dict['some_float'], message.some_float)
self.assertEqual(message_dict['some_datetime'], message.some_datetime)
self.assertEqual(message_dict['some_timedelta'], message.some_timedelta)

def test__to_json_dict__okay(self):
# Arrange
message = example_message()

# Act
message_dict = message.to_json_dict()

# Assert
json.dumps(message_dict)
self.assertEqual(message_dict['some_uuid'], str(message.some_uuid))
self.assertEqual(message_dict['some_str'], message.some_str)
self.assertEqual(message_dict['some_float'], message.some_float)
self.assertEqual(message_dict['some_datetime'], message.some_datetime.isoformat())
self.assertEqual(message_dict['some_timedelta'], 'PT1H1M')