diff --git a/dev-requirements.txt b/dev-requirements.txt index 6a82446..f38a575 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -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 @@ -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 @@ -99,6 +100,7 @@ packaging==24.2 # datamodel-code-generator # pyproject-api # pytest + # setuptools-scm # sphinx # tox pathspec==0.12.1 @@ -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 @@ -208,6 +210,7 @@ tomli==2.2.1 # pylint # pyproject-api # pytest + # setuptools-scm # tox tomlkit==0.13.2 # via pylint @@ -217,7 +220,6 @@ types-pytz==2024.2.0.20241221 # via s2-python (pyproject.toml) typing-extensions==4.12.2 # via - # annotated-types # astroid # black # mypy @@ -225,6 +227,8 @@ typing-extensions==4.12.2 # pydantic-core # pylint # pyright + # s2-python (pyproject.toml) + # setuptools-scm # tox urllib3==2.2.3 # via requests @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 8cd822b..e546a09 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "pydantic>=2.8.2", "pytz", "click", + "typing-extensions", ] classifiers = [ "Development Status :: 4 - Beta", @@ -52,6 +53,7 @@ development = [ "datamodel-code-generator", "pre-commit", "tox", + "setuptools_scm", ] docs = [ "sphinx", diff --git a/src/s2python/message.py b/src/s2python/message.py index 3467a57..0fca2be 100644 --- a/src/s2python/message.py +++ b/src/s2python/message.py @@ -1,5 +1,7 @@ from typing import Union +from typing_extensions import TypeAlias + from s2python.frbc import ( FRBCActuatorDescription, FRBCActuatorStatus, @@ -76,7 +78,7 @@ Transition, ) -S2Message = Union[ +S2MessageWithID: TypeAlias = Union[ DDBCAverageDemandRateForecast, DDBCInstruction, DDBCSystemDescription, @@ -111,6 +113,10 @@ InstructionStatusUpdate, PowerForecast, PowerMeasurement, +] + +S2Message: TypeAlias = Union[ + S2MessageWithID, ReceptionStatus, ] diff --git a/src/s2python/ppbc/ppbc_end_interruption_instruction.py b/src/s2python/ppbc/ppbc_end_interruption_instruction.py index d38a454..1a4e3ff 100644 --- a/src/s2python/ppbc/ppbc_end_interruption_instruction.py +++ b/src/s2python/ppbc/ppbc_end_interruption_instruction.py @@ -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] diff --git a/src/s2python/ppbc/ppbc_power_profile_status.py b/src/s2python/ppbc/ppbc_power_profile_status.py index a43661f..5d7d1be 100644 --- a/src/s2python/ppbc/ppbc_power_profile_status.py +++ b/src/s2python/ppbc/ppbc_power_profile_status.py @@ -1,4 +1,5 @@ from typing import List +import uuid from s2python.generated.gen_s2 import ( PPBCPowerProfileStatus as GenPPBCPowerProfileStatus, @@ -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] ) diff --git a/src/s2python/ppbc/ppbc_start_interruption_instruction.py b/src/s2python/ppbc/ppbc_start_interruption_instruction.py index a9fdea6..42ccb92 100644 --- a/src/s2python/ppbc/ppbc_start_interruption_instruction.py +++ b/src/s2python/ppbc/ppbc_start_interruption_instruction.py @@ -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] diff --git a/src/s2python/validate_values_mixin.py b/src/s2python/validate_values_mixin.py index 2d4a644..f140205 100644 --- a/src/s2python/validate_values_mixin.py +++ b/src/s2python/validate_values_mixin.py @@ -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: @@ -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: diff --git a/tests/unit/message_test.py b/tests/unit/message_test.py index 1d3c4a9..eac4a4f 100644 --- a/tests/unit/message_test.py +++ b/tests/unit/message_test.py @@ -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 @@ -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')) diff --git a/tests/unit/validate_values_mixing_test.py b/tests/unit/validate_values_mixing_test.py new file mode 100644 index 0000000..110b18d --- /dev/null +++ b/tests/unit/validate_values_mixing_test.py @@ -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')