diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b69b506..d7348e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -83,6 +83,11 @@ jobs: pipx run --python '${{ steps.setup-python.outputs.python-path }}' tox --installpkg '${{ needs.prepare.outputs.wheel-distribution }}' -- -rFEx --durations 10 --color yes # pytest args + - name: Run tests (s2) + run: >- + pipx run --python '${{ steps.setup-python.outputs.python-path }}' + tox -e s2 --installpkg '${{ needs.prepare.outputs.wheel-distribution }}' + -- -rFEx --durations 10 --color yes # pytest args # - name: Generate coverage report # run: pipx run coverage lcov -o coverage.lcov # - name: Upload partial coverage report diff --git a/README.rst b/README.rst index cc60f9c..035f3da 100644 --- a/README.rst +++ b/README.rst @@ -56,6 +56,10 @@ Install using ``pip``:: pip install flexmeasures-client +To enable S2 features, you need to install extra requirements:: + + pip install flexmeasures-client[s2] + Initialization and authentication:: from flexmeasures_client import FlexMeasuresClient @@ -144,6 +148,13 @@ If you want to develop this package it's necessary to install testing requiremen pip install -e ".[testing]" +Moreover, if you need to work on S2 features, you need to install extra dependencies:: + + pip install -e ".[s2, testing]" + + + + .. _pyscaffold-notes: @@ -163,6 +174,9 @@ Running tests locally is crucial as well. Staying close to the CI workflow:: tox -e clean,build tox -- -rFEx --durations 10 --color yes +For S2 features, you need to add `-e s2` to tox:: + + tox -e s2 This project uses `pre-commit`_, please make sure to install it before making any changes:: diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..820dca4 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --ignore=tests/s2 diff --git a/setup.cfg b/setup.cfg index 7583f47..7417c6f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -51,7 +51,6 @@ install_requires = importlib-metadata; python_version<"3.8" aiohttp pandas>=2.1.4 - s2-python>=0.4.1 # reason: be compatible with constraint pydantic>=2.8.2 async_timeout [options.packages.find] @@ -61,8 +60,8 @@ exclude = [options.extras_require] # Add here additional requirements for extra features, to install with: -# `pip install flexmeasures-client[PDF]` like: -# PDF = ReportLab; RXP +# `pip install flexmeasures-client[s2]` like: +s2=s2-python>=0.4.1 # reason: be compatible with constraint pydantic>=2.8.2 # Add here test requirements (semicolon/line-separated) testing = @@ -73,7 +72,6 @@ testing = pytest-mock aioresponses - [options.entry_points] # Add here console scripts like: # console_scripts = @@ -85,6 +83,7 @@ testing = # pyscaffold.cli = # awesome = pyscaffoldext.awesome.extension:AwesomeExtension + [tool:pytest] # Specify command line options as you would do when invoking pytest directly. # e.g. --cov-report html (or xml) for html/xml output or --junitxml junit.xml diff --git a/src/flexmeasures_client/s2/__init__.py b/src/flexmeasures_client/s2/__init__.py index 66117f5..5b90b45 100644 --- a/src/flexmeasures_client/s2/__init__.py +++ b/src/flexmeasures_client/s2/__init__.py @@ -7,7 +7,15 @@ from typing import Callable, Coroutine, Dict, Type import pydantic -from s2python.common import ReceptionStatus, ReceptionStatusValues, RevokeObject + +try: + from s2python.common import ReceptionStatus, ReceptionStatusValues, RevokeObject +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + from flexmeasures_client.s2.utils import ( SizeLimitOrderedDict, diff --git a/src/flexmeasures_client/s2/cem.py b/src/flexmeasures_client/s2/cem.py index 8d40280..4d3fb4e 100644 --- a/src/flexmeasures_client/s2/cem.py +++ b/src/flexmeasures_client/s2/cem.py @@ -7,17 +7,25 @@ from typing import Dict, Optional import pydantic -from s2python.common import ( - ControlType, - Handshake, - HandshakeResponse, - PowerMeasurement, - ReceptionStatus, - ReceptionStatusValues, - ResourceManagerDetails, - RevokeObject, - SelectControlType, -) + +try: + from s2python.common import ( + ControlType, + Handshake, + HandshakeResponse, + PowerMeasurement, + ReceptionStatus, + ReceptionStatusValues, + ResourceManagerDetails, + RevokeObject, + SelectControlType, + ) +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + from flexmeasures_client.client import FlexMeasuresClient from flexmeasures_client.s2 import Handler, register diff --git a/src/flexmeasures_client/s2/control_types/FRBC/__init__.py b/src/flexmeasures_client/s2/control_types/FRBC/__init__.py index ad2b8c3..2b4ccf1 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/__init__.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/__init__.py @@ -1,17 +1,25 @@ import asyncio import pydantic -from s2python.common import ControlType, ReceptionStatusValues -from s2python.frbc import ( - FRBCActuatorStatus, - FRBCFillLevelTargetProfile, - FRBCInstruction, - FRBCLeakageBehaviour, - FRBCStorageStatus, - FRBCSystemDescription, - FRBCTimerStatus, - FRBCUsageForecast, -) + +try: + from s2python.common import ControlType, ReceptionStatusValues + from s2python.frbc import ( + FRBCActuatorStatus, + FRBCFillLevelTargetProfile, + FRBCInstruction, + FRBCLeakageBehaviour, + FRBCStorageStatus, + FRBCSystemDescription, + FRBCTimerStatus, + FRBCUsageForecast, + ) +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + from flexmeasures_client.s2 import SizeLimitOrderedDict, register from flexmeasures_client.s2.control_types import ControlTypeHandler diff --git a/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py b/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py index 896c281..dcb66c7 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/frbc_simple.py @@ -6,7 +6,19 @@ from datetime import datetime, timedelta import pytz -from s2python.frbc import FRBCActuatorStatus, FRBCStorageStatus, FRBCSystemDescription + +try: + from s2python.frbc import ( + FRBCActuatorStatus, + FRBCStorageStatus, + FRBCSystemDescription, + ) +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + from flexmeasures_client.s2.control_types.FRBC import FRBC from flexmeasures_client.s2.control_types.FRBC.utils import fm_schedule_to_instructions diff --git a/src/flexmeasures_client/s2/control_types/FRBC/frbc_tunes.py b/src/flexmeasures_client/s2/control_types/FRBC/frbc_tunes.py index e4b64ce..889e73b 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/frbc_tunes.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/frbc_tunes.py @@ -10,15 +10,23 @@ import pandas as pd import pydantic import pytz -from s2python.common import NumberRange, ReceptionStatus, ReceptionStatusValues -from s2python.frbc import ( - FRBCActuatorStatus, - FRBCFillLevelTargetProfile, - FRBCInstruction, - FRBCStorageStatus, - FRBCSystemDescription, - FRBCUsageForecast, -) + +try: + from s2python.common import NumberRange, ReceptionStatus, ReceptionStatusValues + from s2python.frbc import ( + FRBCActuatorStatus, + FRBCFillLevelTargetProfile, + FRBCInstruction, + FRBCStorageStatus, + FRBCSystemDescription, + FRBCUsageForecast, + ) +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + from flexmeasures_client.s2 import register from flexmeasures_client.s2.control_types.FRBC import FRBC diff --git a/src/flexmeasures_client/s2/control_types/FRBC/utils.py b/src/flexmeasures_client/s2/control_types/FRBC/utils.py index 22ea836..1b732c9 100644 --- a/src/flexmeasures_client/s2/control_types/FRBC/utils.py +++ b/src/flexmeasures_client/s2/control_types/FRBC/utils.py @@ -3,7 +3,15 @@ from typing import List import pandas as pd -from s2python.frbc import FRBCInstruction, FRBCOperationMode, FRBCSystemDescription + +try: + from s2python.frbc import FRBCInstruction, FRBCOperationMode, FRBCSystemDescription +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + from flexmeasures_client.s2.utils import get_unique_id diff --git a/src/flexmeasures_client/s2/control_types/translations.py b/src/flexmeasures_client/s2/control_types/translations.py index db22bcb..87f6ade 100644 --- a/src/flexmeasures_client/s2/control_types/translations.py +++ b/src/flexmeasures_client/s2/control_types/translations.py @@ -4,11 +4,18 @@ import numpy as np import pandas as pd -from s2python.frbc import ( - FRBCFillLevelTargetProfile, - FRBCLeakageBehaviour, - FRBCUsageForecast, -) + +try: + from s2python.frbc import ( + FRBCFillLevelTargetProfile, + FRBCLeakageBehaviour, + FRBCUsageForecast, + ) +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) def leakage_behaviour_to_storage_efficieny( diff --git a/src/flexmeasures_client/s2/utils.py b/src/flexmeasures_client/s2/utils.py index 42f0347..12cc9e7 100644 --- a/src/flexmeasures_client/s2/utils.py +++ b/src/flexmeasures_client/s2/utils.py @@ -5,7 +5,15 @@ from uuid import uuid4 import pydantic -from s2python.common import ReceptionStatus, ReceptionStatusValues + +try: + from s2python.common import ReceptionStatus, ReceptionStatusValues +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) + KT = TypeVar("KT") VT = TypeVar("VT") diff --git a/src/flexmeasures_client/s2/wrapper.py b/src/flexmeasures_client/s2/wrapper.py index 0a4d9c0..97acf28 100644 --- a/src/flexmeasures_client/s2/wrapper.py +++ b/src/flexmeasures_client/s2/wrapper.py @@ -1,7 +1,14 @@ from datetime import datetime from pydantic import BaseModel, Field -from s2python.message import S2Message + +try: + from s2python.message import S2Message +except ImportError: + raise ImportError( + "The 's2-python' package is required for this functionality. " + "Install it using `pip install flexmeasures-client[s2]`." + ) class MetaData(BaseModel): diff --git a/tests/conftest.py b/tests/conftest.py index 7480690..e69de29 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,121 +0,0 @@ -from __future__ import annotations - -from datetime import datetime, timezone - -import pytest -from s2python.common import ( - Commodity, - CommodityQuantity, - ControlType, - Duration, - EnergyManagementRole, - Handshake, - NumberRange, - PowerRange, - ResourceManagerDetails, - Role, - RoleType, -) -from s2python.frbc import ( - FRBCActuatorDescription, - FRBCOperationMode, - FRBCOperationModeElement, - FRBCStorageDescription, - FRBCSystemDescription, -) - -from flexmeasures_client.s2.utils import get_unique_id - - -@pytest.fixture(scope="session") -def frbc_system_description(): - ######## - # FRBC # - ######## - - thp_operation_mode_element = FRBCOperationModeElement( - fill_level_range=NumberRange(start_of_range=0, end_of_range=80), - fill_rate=NumberRange(start_of_range=0, end_of_range=2), - power_ranges=[ - PowerRange( - start_of_range=10, - end_of_range=1000, - commodity_quantity=CommodityQuantity.ELECTRIC_POWER_3_PHASE_SYMMETRIC, - ) - ], - ) - - thp_operation_mode = FRBCOperationMode( - id=get_unique_id(), - elements=[thp_operation_mode_element], - abnormal_condition_only=False, - ) - - nes_operation_mode_element = FRBCOperationModeElement( - fill_level_range=NumberRange(start_of_range=0, end_of_range=100), - fill_rate=NumberRange(start_of_range=0, end_of_range=1), - power_ranges=[ - PowerRange( - start_of_range=10, - end_of_range=1000, - commodity_quantity=CommodityQuantity.ELECTRIC_POWER_3_PHASE_SYMMETRIC, - ) - ], - ) - - nes_operation_mode = FRBCOperationMode( - id=get_unique_id(), - elements=[nes_operation_mode_element], - abnormal_condition_only=False, - ) - - actuator = FRBCActuatorDescription( - id=get_unique_id(), - supported_commodities=[Commodity.ELECTRICITY], - operation_modes=[thp_operation_mode, nes_operation_mode], - transitions=[], - timers=[], - ) - - storage = FRBCStorageDescription( - provides_leakage_behaviour=True, - provides_fill_level_target_profile=True, - provides_usage_forecast=True, - fill_level_range=NumberRange(start_of_range=0, end_of_range=1), - ) - - system_description_message = FRBCSystemDescription( - message_id=get_unique_id(), - valid_from=datetime(2024, 1, 1, tzinfo=timezone.utc), - actuators=[actuator], - storage=storage, - ) - - return system_description_message - - -@pytest.fixture(scope="session") -def resource_manager_details(): - return ResourceManagerDetails( - message_id=get_unique_id(), - resource_id=get_unique_id(), - roles=[Role(role=RoleType.ENERGY_STORAGE, commodity=Commodity.ELECTRICITY)], - instruction_processing_delay=Duration(1), - available_control_types=[ - ControlType.FILL_RATE_BASED_CONTROL, - ControlType.NO_SELECTION, - ], - provides_forecast=True, - provides_power_measurement_types=[ - CommodityQuantity.ELECTRIC_POWER_3_PHASE_SYMMETRIC - ], - ) - - -@pytest.fixture(scope="session") -def rm_handshake(): - return Handshake( - message_id=get_unique_id(), - role=EnergyManagementRole.RM, - supported_protocol_versions=["1.0.0"], - ) diff --git a/tests/s2/__init__.py b/tests/s2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/s2/conftest.py b/tests/s2/conftest.py new file mode 100644 index 0000000..7480690 --- /dev/null +++ b/tests/s2/conftest.py @@ -0,0 +1,121 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +import pytest +from s2python.common import ( + Commodity, + CommodityQuantity, + ControlType, + Duration, + EnergyManagementRole, + Handshake, + NumberRange, + PowerRange, + ResourceManagerDetails, + Role, + RoleType, +) +from s2python.frbc import ( + FRBCActuatorDescription, + FRBCOperationMode, + FRBCOperationModeElement, + FRBCStorageDescription, + FRBCSystemDescription, +) + +from flexmeasures_client.s2.utils import get_unique_id + + +@pytest.fixture(scope="session") +def frbc_system_description(): + ######## + # FRBC # + ######## + + thp_operation_mode_element = FRBCOperationModeElement( + fill_level_range=NumberRange(start_of_range=0, end_of_range=80), + fill_rate=NumberRange(start_of_range=0, end_of_range=2), + power_ranges=[ + PowerRange( + start_of_range=10, + end_of_range=1000, + commodity_quantity=CommodityQuantity.ELECTRIC_POWER_3_PHASE_SYMMETRIC, + ) + ], + ) + + thp_operation_mode = FRBCOperationMode( + id=get_unique_id(), + elements=[thp_operation_mode_element], + abnormal_condition_only=False, + ) + + nes_operation_mode_element = FRBCOperationModeElement( + fill_level_range=NumberRange(start_of_range=0, end_of_range=100), + fill_rate=NumberRange(start_of_range=0, end_of_range=1), + power_ranges=[ + PowerRange( + start_of_range=10, + end_of_range=1000, + commodity_quantity=CommodityQuantity.ELECTRIC_POWER_3_PHASE_SYMMETRIC, + ) + ], + ) + + nes_operation_mode = FRBCOperationMode( + id=get_unique_id(), + elements=[nes_operation_mode_element], + abnormal_condition_only=False, + ) + + actuator = FRBCActuatorDescription( + id=get_unique_id(), + supported_commodities=[Commodity.ELECTRICITY], + operation_modes=[thp_operation_mode, nes_operation_mode], + transitions=[], + timers=[], + ) + + storage = FRBCStorageDescription( + provides_leakage_behaviour=True, + provides_fill_level_target_profile=True, + provides_usage_forecast=True, + fill_level_range=NumberRange(start_of_range=0, end_of_range=1), + ) + + system_description_message = FRBCSystemDescription( + message_id=get_unique_id(), + valid_from=datetime(2024, 1, 1, tzinfo=timezone.utc), + actuators=[actuator], + storage=storage, + ) + + return system_description_message + + +@pytest.fixture(scope="session") +def resource_manager_details(): + return ResourceManagerDetails( + message_id=get_unique_id(), + resource_id=get_unique_id(), + roles=[Role(role=RoleType.ENERGY_STORAGE, commodity=Commodity.ELECTRICITY)], + instruction_processing_delay=Duration(1), + available_control_types=[ + ControlType.FILL_RATE_BASED_CONTROL, + ControlType.NO_SELECTION, + ], + provides_forecast=True, + provides_power_measurement_types=[ + CommodityQuantity.ELECTRIC_POWER_3_PHASE_SYMMETRIC + ], + ) + + +@pytest.fixture(scope="session") +def rm_handshake(): + return Handshake( + message_id=get_unique_id(), + role=EnergyManagementRole.RM, + supported_protocol_versions=["1.0.0"], + ) diff --git a/tests/test_cem.py b/tests/s2/test_cem.py similarity index 100% rename from tests/test_cem.py rename to tests/s2/test_cem.py diff --git a/tests/test_frbc_tunes.py b/tests/s2/test_frbc_tunes.py similarity index 100% rename from tests/test_frbc_tunes.py rename to tests/s2/test_frbc_tunes.py diff --git a/tests/test_frbc_utils.py b/tests/s2/test_frbc_utils.py similarity index 100% rename from tests/test_frbc_utils.py rename to tests/s2/test_frbc_utils.py diff --git a/tests/test_s2_models.py b/tests/s2/test_s2_models.py similarity index 100% rename from tests/test_s2_models.py rename to tests/s2/test_s2_models.py diff --git a/tests/test_s2_translations.py b/tests/s2/test_s2_translations.py similarity index 100% rename from tests/test_s2_translations.py rename to tests/s2/test_s2_translations.py diff --git a/tox.ini b/tox.ini index 69f8159..abf99fe 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,12 @@ extras = commands = pytest {posargs} +[testenv:s2] +deps = + .[s2] # Install the package with optional dependencies +commands = pytest tests/s2 + + # # To run `tox -e lint` you need to make sure you have a # # `.pre-commit-config.yaml` file. See https://pre-commit.com