From 51c7fa83f53cdc69e69717fcbdb07caa48f5b35d Mon Sep 17 00:00:00 2001 From: maugde <167874615+maugde@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:46:58 +0100 Subject: [PATCH 01/11] fix(commands): remove command application notifications (#2323) The notifications raised errors because the indexing was wrong. Those notifications were anyway only used in the frond for a featues which does not work well, to monitor the progress of the snapshot generations. This will be reworked later when we work on logging more information for the user. --- .../storage/variantstudy/command_factory.py | 2 +- .../variantstudy/snapshot_generator.py | 1 - .../variantstudy/variant_command_generator.py | 16 ++---- .../variantstudy/variant_study_service.py | 51 +++---------------- .../test_variant_study_service.py | 4 +- 5 files changed, 12 insertions(+), 62 deletions(-) diff --git a/antarest/study/storage/variantstudy/command_factory.py b/antarest/study/storage/variantstudy/command_factory.py index 4be8736dab..a9956a86a2 100644 --- a/antarest/study/storage/variantstudy/command_factory.py +++ b/antarest/study/storage/variantstudy/command_factory.py @@ -136,7 +136,7 @@ def to_command(self, command_dto: CommandDTO) -> List[ICommand]: args = command_dto.args if isinstance(args, dict): # In some cases, pydantic can modify inplace the given args. - # We don't want that so before doing so we copy the dictionnary. + # We don't want that so before doing so we copy the dictionary. new_args = copy.deepcopy(args) return [ self._to_single_command( diff --git a/antarest/study/storage/variantstudy/snapshot_generator.py b/antarest/study/storage/variantstudy/snapshot_generator.py index 60e726f2e0..543f5bae5d 100644 --- a/antarest/study/storage/variantstudy/snapshot_generator.py +++ b/antarest/study/storage/variantstudy/snapshot_generator.py @@ -185,7 +185,6 @@ def _apply_commands( snapshot_dir, variant_study, delete_on_failure=False, # Not needed, because we are using a temporary directory - notifier=None, listener=listener, ) if not results.success: diff --git a/antarest/study/storage/variantstudy/variant_command_generator.py b/antarest/study/storage/variantstudy/variant_command_generator.py index d30b77ece0..87820681aa 100644 --- a/antarest/study/storage/variantstudy/variant_command_generator.py +++ b/antarest/study/storage/variantstudy/variant_command_generator.py @@ -9,12 +9,12 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import itertools import logging import shutil import uuid from pathlib import Path -from typing import Callable, List, Optional, Tuple, Union, cast +from typing import Callable, Dict, List, Optional, Set, Tuple, Union, cast from antarest.core.utils.utils import StopWatch from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig @@ -51,7 +51,6 @@ def _generate( data: Union[FileStudy, FileStudyTreeConfig], applier: APPLY_CALLBACK, metadata: Optional[VariantStudy] = None, - notifier: Optional[Callable[[int, bool, str], None]] = None, listener: Optional[ICommandListener] = None, ) -> GenerationResultInfoDTO: stopwatch = StopWatch() @@ -61,8 +60,8 @@ def _generate( logger.info("Applying commands") study_id = "-" if metadata is None else metadata.id - # flatten the list of commands - all_commands = [command for command_batch in commands for command in command_batch] + # Flatten the list of commands + all_commands: List[ICommand] = list(itertools.chain.from_iterable(commands)) # Prepare the stopwatch cmd_notifier = CmdNotifier(study_id, len(all_commands)) @@ -89,9 +88,6 @@ def _generate( } results.details.append(detail) - if notifier: - notifier(index - 1, output.status, output.message) - cmd_notifier.index = index stopwatch.log_elapsed(cmd_notifier) @@ -117,7 +113,6 @@ def generate( dest_path: Path, metadata: Optional[VariantStudy] = None, delete_on_failure: bool = True, - notifier: Optional[Callable[[int, bool, str], None]] = None, listener: Optional[ICommandListener] = None, ) -> GenerationResultInfoDTO: # Build file study @@ -131,7 +126,6 @@ def generate( study, lambda command, data, listener: command.apply(cast(FileStudy, data), listener), metadata, - notifier, ) if not results.success and delete_on_failure: @@ -143,7 +137,6 @@ def generate_config( commands: List[List[ICommand]], config: FileStudyTreeConfig, metadata: Optional[VariantStudy] = None, - notifier: Optional[Callable[[int, bool, str], None]] = None, ) -> Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: logger.info("Building config (light generation)") results = VariantCommandGenerator._generate( @@ -151,7 +144,6 @@ def generate_config( config, lambda command, data, listener: command.apply_config(cast(FileStudyTreeConfig, data)), metadata, - notifier, ) # because the config has the parent id there if metadata: diff --git a/antarest/study/storage/variantstudy/variant_study_service.py b/antarest/study/storage/variantstudy/variant_study_service.py index 69f3d43731..0a6ee96d62 100644 --- a/antarest/study/storage/variantstudy/variant_study_service.py +++ b/antarest/study/storage/variantstudy/variant_study_service.py @@ -14,6 +14,7 @@ import logging import re import shutil +import typing as t from datetime import datetime, timedelta from functools import reduce from pathlib import Path @@ -785,41 +786,7 @@ def _generate_study_config( config.study_path = Path(metadata.path) return res, config - def _get_commands_and_notifier( - self, - variant_study: VariantStudy, - notifier: ITaskNotifier, - from_index: int = 0, - ) -> Tuple[List[List[ICommand]], Callable[[int, bool, str], None]]: - # Generate - commands: List[List[ICommand]] = self._to_commands(variant_study, from_index) - - def notify(command_index: int, command_result: bool, command_message: str) -> None: - try: - command_result_obj = CommandResultDTO( - study_id=variant_study.id, - id=variant_study.commands[from_index + command_index].id, - success=command_result, - message=command_message, - ) - notifier.notify_message(command_result_obj.model_dump_json()) - self.event_bus.push( - Event( - type=EventType.STUDY_VARIANT_GENERATION_COMMAND_RESULT, - payload=command_result_obj, - permissions=PermissionInfo.from_study(variant_study), - channel=EventChannelDirectory.STUDY_GENERATION + variant_study.id, - ) - ) - except Exception as e: - logger.error( - f"Fail to notify command result n°{command_index} for study {variant_study.id}", - exc_info=e, - ) - - return commands, notify - - def _to_commands(self, metadata: VariantStudy, from_index: int = 0) -> List[List[ICommand]]: + def _to_commands(self, metadata: VariantStudy, from_index: int = 0) -> t.List[t.List[ICommand]]: commands: List[List[ICommand]] = [ self.command_factory.to_command(command_block.to_dto()) for index, command_block in enumerate(metadata.commands) @@ -831,24 +798,18 @@ def _generate_config( self, variant_study: VariantStudy, config: FileStudyTreeConfig, - notifier: ITaskNotifier = NoopNotifier(), ) -> Tuple[GenerationResultInfoDTO, FileStudyTreeConfig]: - commands, notify = self._get_commands_and_notifier(variant_study=variant_study, notifier=notifier) - return self.generator.generate_config(commands, config, variant_study, notifier=notify) + commands = self._to_commands(variant_study) + return self.generator.generate_config(commands, config, variant_study) def _generate_snapshot( self, variant_study: VariantStudy, dst_path: Path, - notifier: ITaskNotifier = NoopNotifier(), from_command_index: int = 0, ) -> GenerationResultInfoDTO: - commands, notify = self._get_commands_and_notifier( - variant_study=variant_study, - notifier=notifier, - from_index=from_command_index, - ) - return self.generator.generate(commands, dst_path, variant_study, notifier=notify) + commands = self._to_commands(variant_study, from_command_index) + return self.generator.generate(commands, dst_path, variant_study) def get_study_task(self, study_id: str, params: RequestParameters) -> TaskDTO: """ diff --git a/tests/study/storage/variantstudy/test_variant_study_service.py b/tests/study/storage/variantstudy/test_variant_study_service.py index 54e1db01ab..a878a4cdf4 100644 --- a/tests/study/storage/variantstudy/test_variant_study_service.py +++ b/tests/study/storage/variantstudy/test_variant_study_service.py @@ -12,9 +12,8 @@ import datetime import re -import typing from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import Mock import numpy as np import pytest @@ -429,6 +428,5 @@ def utcnow(cls) -> datetime.datetime: variant_study_service.task_service.await_task(task_id) # Check if all snapshots were cleared - nb_snapshot_dir = 0 # after the for iterations, must equal 0 for variant_path in variant_study_path.iterdir(): assert not variant_path.joinpath("snapshot").exists() From e44a0804c91322f85651acfd2fa7f6941be5dd08 Mon Sep 17 00:00:00 2001 From: Sylvain Leclerc Date: Tue, 11 Feb 2025 17:54:08 +0100 Subject: [PATCH 02/11] feat(group): enforce lower case for items group names (#2333) For v8.6 studies, short term storage groups are serialized to title case, in order to comply with the stricter solver format at the time. Some technical improvements along the way: - clarify versioning of commands: serialization format is versioned, not the commands themselves - add and test some annotated type ItemName and AreaId that will be re-usable - remove group from parent class ItemProperties which did not belong there Signed-off-by: Sylvain Leclerc --- antarest/core/model.py | 4 + .../extensions/adequacy_patch/extension.py | 2 +- antarest/study/business/area_management.py | 3 +- .../business/areas/renewable_management.py | 29 +- .../business/areas/st_storage_management.py | 31 +- .../business/areas/thermal_management.py | 33 +- .../business/binding_constraint_management.py | 6 +- antarest/study/business/district_manager.py | 2 +- .../model/filesystem/config/cluster.py | 31 +- .../filesystem/config/field_validators.py | 27 +- .../rawstudy/model/filesystem/config/files.py | 2 +- .../model/filesystem/config/identifier.py | 24 +- .../rawstudy/model/filesystem/config/model.py | 26 +- .../model/filesystem/config/renewable.py | 50 +- .../model/filesystem/config/st_storage.py | 47 +- .../model/filesystem/config/thermal.py | 57 +- .../bindingconstraints_ini.py | 15 +- .../root/input/renewables/clusters.py | 17 +- .../input/st_storage/clusters/area/list.py | 31 +- .../root/input/thermal/cluster/area/list.py | 15 +- .../storage/variantstudy/business/utils.py | 38 +- .../storage/variantstudy/command_factory.py | 24 +- .../variantstudy/model/command/create_area.py | 9 +- .../command/create_binding_constraint.py | 15 +- .../model/command/create_cluster.py | 100 +- .../model/command/create_district.py | 8 +- .../command/create_renewables_cluster.py | 56 +- .../model/command/create_st_storage.py | 52 +- .../model/command/create_user_resource.py | 1 - .../generate_thermal_cluster_timeseries.py | 1 - .../variantstudy/model/command/icommand.py | 2 - .../variantstudy/model/command/remove_area.py | 1 - .../command/remove_binding_constraint.py | 1 - .../model/command/remove_cluster.py | 1 - .../model/command/remove_district.py | 1 - .../variantstudy/model/command/remove_link.py | 4 +- .../remove_multiple_binding_constraints.py | 1 - .../command/remove_renewables_cluster.py | 1 - .../model/command/remove_st_storage.py | 1 - .../model/command/remove_user_resource.py | 1 - .../model/command/replace_matrix.py | 1 - .../model/command/update_area_ui.py | 1 - .../command/update_binding_constraint.py | 3 +- .../model/command/update_comments.py | 1 - .../model/command/update_config.py | 1 - .../model/command/update_district.py | 1 - .../variantstudy/model/command/update_link.py | 1 - .../model/command/update_playlist.py | 1 - .../model/command/update_raw_file.py | 1 - .../model/command/update_scenario_builder.py | 1 - .../study/storage/variantstudy/model/model.py | 7 +- antarest/study/web/study_data_blueprint.py | 2 +- .../test_synthesis/raw_study.synthesis.json | 72 +- .../variant_study.synthesis.json | 72 +- .../test_binding_constraints.py | 8 +- .../study_data_blueprint/test_renewable.py | 33 +- .../study_data_blueprint/test_st_storage.py | 21 +- .../study_data_blueprint/test_table_mode.py | 110 +- .../study_data_blueprint/test_thermal.py | 26 +- tests/integration/test_integration.py | 62 +- .../test_integration_token_end_to_end.py | 18 +- .../test_renewable_cluster.py | 35 +- .../variant_blueprint/test_st_storage.py | 6 +- .../variant_blueprint/test_thermal_cluster.py | 2 +- .../business/test_study_version_upgrader.py | 2 +- .../filesystem/config/test_utils.py | 2 +- .../special_node/test_lower_case_nodes.py | 165 +++ .../rawstudy/test_raw_study_service.py | 9 +- .../variantstudy/test_snapshot_generator.py | 52 +- .../test_variant_study_service.py | 9 +- tests/study/test_field_validators.py | 47 + .../model/command/test_create_area.py | 3 +- .../model/command/test_create_cluster.py | 110 +- .../model/command/test_create_link.py | 2 +- .../command/test_create_renewables_cluster.py | 55 +- .../model/command/test_create_st_storage.py | 70 +- .../test_manage_binding_constraints.py | 13 +- .../model/command/test_manage_district.py | 2 +- .../model/command/test_remove_area.py | 37 +- .../model/command/test_remove_cluster.py | 27 +- .../model/command/test_remove_link.py | 2 +- .../command/test_remove_renewables_cluster.py | 15 +- .../model/command/test_remove_st_storage.py | 3 +- .../model/command/test_replace_matrix.py | 2 +- .../model/command/test_update_config.py | 2 +- tests/variantstudy/test_command_factory.py | 1086 +++++++++++------ 86 files changed, 1928 insertions(+), 1043 deletions(-) create mode 100644 tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py create mode 100644 tests/study/test_field_validators.py diff --git a/antarest/core/model.py b/antarest/core/model.py index 7f0b64f7db..67d287f4e7 100644 --- a/antarest/core/model.py +++ b/antarest/core/model.py @@ -13,6 +13,9 @@ import enum from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union +import typing_extensions as te +from pydantic import StringConstraints + from antarest.core.serde import AntaresBaseModel if TYPE_CHECKING: @@ -22,6 +25,7 @@ JSON = Dict[str, Any] ELEMENT = Union[str, int, float, bool, bytes] SUB_JSON = Union[ELEMENT, JSON, List[Any], None] +LowerCaseStr = te.Annotated[str, StringConstraints(to_lower=True)] class PublicMode(enum.StrEnum): diff --git a/antarest/launcher/extensions/adequacy_patch/extension.py b/antarest/launcher/extensions/adequacy_patch/extension.py index 335677c5de..a3cee2cde1 100644 --- a/antarest/launcher/extensions/adequacy_patch/extension.py +++ b/antarest/launcher/extensions/adequacy_patch/extension.py @@ -24,7 +24,7 @@ from antarest.core.utils.utils import assert_this from antarest.launcher.extensions.interface import ILauncherExtension from antarest.study.service import StudyService -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy logger = logging.getLogger(__name__) diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 21447352d2..15cd7e61d7 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -34,7 +34,8 @@ ThermalAreasProperties, UIProperties, ) -from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, DistrictSet, transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, DistrictSet from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/antarest/study/business/areas/renewable_management.py b/antarest/study/business/areas/renewable_management.py index e3705e23ff..81a41147f6 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -22,12 +22,14 @@ from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import Study -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( RenewableConfig, RenewableConfigType, RenewableProperties, + RenewablePropertiesType, create_renewable_config, + create_renewable_properties, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -73,19 +75,9 @@ class RenewableClusterCreation(RenewableClusterInput): Model representing the data structure required to create a new Renewable cluster within a study. """ - # noinspection Pydantic - @field_validator("name", mode="before") - def validate_name(cls, name: Optional[str]) -> str: - """ - Validator to check if the name is not empty. - """ - if not name: - raise ValueError("name must not be empty") - return name - - def to_config(self, study_version: StudyVersion) -> RenewableConfigType: + def to_properties(self, study_version: StudyVersion) -> RenewablePropertiesType: values = self.model_dump(by_alias=False, exclude_none=True) - return create_renewable_config(study_version=study_version, **values) + return create_renewable_properties(study_version=study_version, data=values) @all_optional_model @@ -206,7 +198,7 @@ def create_cluster( The newly created cluster. """ file_study = self._get_file_study(study) - cluster = cluster_data.to_config(StudyVersion.parse(study.version)) + cluster = cluster_data.to_properties(StudyVersion.parse(study.version)) command = self._make_create_cluster_cmd(area_id, cluster, file_study.config.version) execute_or_add_commands( study, @@ -214,16 +206,15 @@ def create_cluster( [command], self.storage_service, ) - output = self.get_cluster(study, area_id, cluster.id) + output = self.get_cluster(study, area_id, cluster.get_id()) return output def _make_create_cluster_cmd( - self, area_id: str, cluster: RenewableConfigType, study_version: StudyVersion + self, area_id: str, cluster: RenewablePropertiesType, study_version: StudyVersion ) -> CreateRenewablesCluster: command = CreateRenewablesCluster( area_id=area_id, - cluster_name=cluster.id, - parameters=cluster.model_dump(mode="json", by_alias=True, exclude={"id"}), + parameters=cluster, command_context=self.storage_service.variant_study_service.command_factory.command_context, study_version=study_version, ) @@ -367,7 +358,7 @@ def duplicate_cluster( current_cluster = self.get_cluster(study, area_id, source_id) current_cluster.name = new_cluster_name creation_form = RenewableClusterCreation(**current_cluster.model_dump(by_alias=False, exclude={"id"})) - new_config = creation_form.to_config(study_version) + new_config = creation_form.to_properties(study_version) create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) # Matrix edition diff --git a/antarest/study/business/areas/st_storage_management.py b/antarest/study/business/areas/st_storage_management.py index 598e0aad7f..681649970c 100644 --- a/antarest/study/business/areas/st_storage_management.py +++ b/antarest/study/business/areas/st_storage_management.py @@ -33,13 +33,14 @@ from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import STUDY_VERSION_8_8, Study -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( STStorage880Config, STStorage880Properties, - STStorageConfigType, STStorageGroup, + STStoragePropertiesType, create_st_storage_config, + create_st_storage_properties, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -78,6 +79,7 @@ class STStorageCreation(STStorageInput): # noinspection Pydantic @field_validator("name", mode="before") + @classmethod def validate_name(cls, name: Optional[str]) -> str: """ Validator to check if the name is not empty. @@ -86,10 +88,10 @@ def validate_name(cls, name: Optional[str]) -> str: raise ValueError("'name' must not be empty") return name - # noinspection PyUnusedLocal - def to_config(self, study_version: StudyVersion) -> STStorageConfigType: - values = self.model_dump(mode="json", by_alias=False, exclude_none=True) - return create_st_storage_config(study_version=study_version, **values) + def to_properties(self, version: StudyVersion) -> STStoragePropertiesType: + return create_st_storage_properties( + study_version=version, data=self.model_dump(mode="json", by_alias=False, exclude_none=True) + ) @all_optional_model @@ -285,10 +287,11 @@ def create_storage( file_study = self._get_file_study(study) values_by_ids = _get_values_by_ids(file_study, area_id) - storage = form.to_config(StudyVersion.parse(study.version)) - values = values_by_ids.get(storage.id) + storage = form.to_properties(StudyVersion.parse(study.version)) + storage_id = storage.get_id() + values = values_by_ids.get(storage_id) if values is not None: - raise DuplicateSTStorage(area_id, storage.id) + raise DuplicateSTStorage(area_id, storage_id) command = self._make_create_cluster_cmd(area_id, storage, file_study.config.version) execute_or_add_commands( @@ -297,11 +300,11 @@ def create_storage( [command], self.storage_service, ) - output = self.get_storage(study, area_id, storage_id=storage.id) + output = self.get_storage(study, area_id, storage_id=storage_id) return output def _make_create_cluster_cmd( - self, area_id: str, cluster: STStorageConfigType, study_version: StudyVersion + self, area_id: str, cluster: STStoragePropertiesType, study_version: StudyVersion ) -> CreateSTStorage: command = CreateSTStorage( area_id=area_id, @@ -563,11 +566,11 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus study_version = StudyVersion.parse(study.version) if study_version < STUDY_VERSION_8_8: fields_to_exclude.add("enabled") - creation_form = STStorageCreation( - **current_cluster.model_dump(mode="json", by_alias=False, exclude=fields_to_exclude) + creation_form = STStorageCreation.model_validate( + current_cluster.model_dump(mode="json", by_alias=False, exclude=fields_to_exclude) ) - new_config = creation_form.to_config(study_version) + new_config = creation_form.to_properties(study_version) create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) # Matrix edition diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 40cc6bf2f6..11531eb9fd 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -28,12 +28,13 @@ from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import STUDY_VERSION_8_7, Study -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( Thermal870Config, Thermal870Properties, - ThermalConfigType, + ThermalPropertiesType, create_thermal_config, + create_thermal_properties, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -83,6 +84,7 @@ class ThermalClusterCreation(ThermalClusterInput): # noinspection Pydantic @field_validator("name", mode="before") + @classmethod def validate_name(cls, name: Optional[str]) -> str: """ Validator to check if the name is not empty. @@ -91,9 +93,9 @@ def validate_name(cls, name: Optional[str]) -> str: raise ValueError("name must not be empty") return name - def to_config(self, study_version: StudyVersion) -> ThermalConfigType: + def to_properties(self, study_version: StudyVersion) -> ThermalPropertiesType: values = self.model_dump(mode="json", by_alias=False, exclude_none=True) - return create_thermal_config(study_version=study_version, **values) + return create_thermal_properties(study_version=study_version, data=values) @all_optional_model @@ -298,7 +300,7 @@ def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalCluste """ file_study = self._get_file_study(study) - cluster = cluster_data.to_config(StudyVersion.parse(study.version)) + cluster = cluster_data.to_properties(StudyVersion.parse(study.version)) command = self._make_create_cluster_cmd(area_id, cluster, file_study.config.version) execute_or_add_commands( study, @@ -306,23 +308,20 @@ def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalCluste [command], self.storage_service, ) - output = self.get_cluster(study, area_id, cluster.id) + output = self.get_cluster(study, area_id, cluster.get_id()) return output def _make_create_cluster_cmd( - self, area_id: str, cluster: ThermalConfigType, study_version: StudyVersion + self, area_id: str, cluster: ThermalPropertiesType, study_version: StudyVersion ) -> CreateCluster: # NOTE: currently, in the `CreateCluster` class, there is a confusion # between the cluster name and the cluster ID (which is a section name). - args = { - "area_id": area_id, - "cluster_name": cluster.id, - "parameters": cluster.model_dump(mode="json", by_alias=True, exclude={"id"}), - "command_context": self.storage_service.variant_study_service.command_factory.command_context, - "study_version": study_version, - } - command = CreateCluster.model_validate(args) - return command + return CreateCluster( + area_id=area_id, + parameters=cluster, + study_version=study_version, + command_context=self.storage_service.variant_study_service.command_factory.command_context, + ) def update_cluster( self, @@ -441,7 +440,7 @@ def duplicate_cluster( source_cluster.name = new_cluster_name creation_form = ThermalClusterCreation(**source_cluster.model_dump(mode="json", by_alias=False, exclude={"id"})) study_version = StudyVersion.parse(study.version) - new_config = creation_form.to_config(study_version) + new_config = creation_form.to_properties(study_version) create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) # Matrix edition diff --git a/antarest/study/business/binding_constraint_management.py b/antarest/study/business/binding_constraint_management.py index 9e8204c78f..08cde618d6 100644 --- a/antarest/study/business/binding_constraint_management.py +++ b/antarest/study/business/binding_constraint_management.py @@ -30,7 +30,7 @@ MatrixWidthMismatchError, WrongMatrixHeightError, ) -from antarest.core.model import JSON +from antarest.core.model import JSON, LowerCaseStr from antarest.core.requests import CaseInsensitiveDict from antarest.core.serde import AntaresBaseModel from antarest.core.utils.string import to_camel_case @@ -44,7 +44,7 @@ BindingConstraintFrequency, BindingConstraintOperator, ) -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.business.matrix_constants.binding_constraint.series_after_v87 import ( @@ -343,7 +343,7 @@ class ConstraintOutput830(ConstraintOutputBase): class ConstraintOutput870(ConstraintOutput830): - group: str = DEFAULT_GROUP + group: LowerCaseStr = DEFAULT_GROUP # WARNING: Do not change the order of the following line, it is used to determine diff --git a/antarest/study/business/district_manager.py b/antarest/study/business/district_manager.py index a10628ad6a..773a7e6056 100644 --- a/antarest/study/business/district_manager.py +++ b/antarest/study/business/district_manager.py @@ -16,7 +16,7 @@ from antarest.core.serde import AntaresBaseModel from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import Study -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_district import CreateDistrict, DistrictBaseFilter from antarest.study.storage.variantstudy.model.command.remove_district import RemoveDistrict diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py index 4c7745df0e..6950e0f4b4 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py @@ -16,15 +16,12 @@ In the near future, this set of classes may be used for solar, wind and hydro clusters. """ -import functools -from typing import Any - from pydantic import Field from antarest.core.serde import AntaresBaseModel +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import ItemName -@functools.total_ordering class ItemProperties( AntaresBaseModel, extra="forbid", @@ -33,33 +30,9 @@ class ItemProperties( ): """ Common properties related to thermal and renewable clusters, and short-term storage. - - Usage: - - >>> from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ItemProperties - - >>> cl1 = ItemProperties(name="cluster-01", group="group-A") - >>> cl2 = ItemProperties(name="CLUSTER-01", group="Group-B") - >>> cl3 = ItemProperties(name="cluster-02", group="GROUP-A") - >>> l = [cl1, cl2, cl3] - >>> l.sort() - >>> [(c.group, c.name) for c in l] - [('group-A', 'cluster-01'), ('GROUP-A', 'cluster-02'), ('Group-B', 'CLUSTER-01')] """ - group: str = Field(default="", description="Cluster group") - - name: str = Field(description="Cluster name", pattern=r"[a-zA-Z0-9_(),& -]+") - - def __lt__(self, other: Any) -> bool: - """ - Compare two clusters by group and name. - - This method may be used to sort and group clusters by `group` and `name`. - """ - if isinstance(other, ItemProperties): - return (self.group.upper(), self.name.upper()).__lt__((other.group.upper(), other.name.upper())) - return NotImplemented + name: ItemName class ClusterProperties(ItemProperties): diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py b/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py index 83dc031f54..cefd32439e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py @@ -9,12 +9,37 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import re +from typing import Annotated, Any, List, Mapping, MutableMapping -from typing import Any, List, Mapping, MutableMapping +from pydantic import BeforeValidator, Field + +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id _ALL_FILTERING = ["hourly", "daily", "weekly", "monthly", "annual"] +def _validate_item_name(name: Any) -> str: + if isinstance(name, int): + name = str(name) + if not isinstance(name, str): + raise ValueError(f"Invalid name '{name}'.") + if not transform_name_to_id(name): + raise ValueError(f"Invalid name '{name}'.") + return name + + +# Type to be used for item names, will raise an error if name +# does not comply with antares-simulator limitations. +ItemName = Annotated[str, BeforeValidator(_validate_item_name)] + +# Type to be used for area identifiers. An ID is valid if it contains +# only lower case alphanumeric characters, parenthesis, comma, +# ampersand, spaces, underscores, or dashes, as defined by +# antares-simulator. +AreaId = Annotated[str, Field(description="Area ID", pattern=r"^[a-z0-9_(),& -]+$")] + + def extract_filtering(v: Any) -> List[str]: """ Extract filtering values from a comma-separated list of values. diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/files.py b/antarest/study/storage/rawstudy/model/filesystem/config/files.py index c5c62e3380..8d125e5196 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/files.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/files.py @@ -37,6 +37,7 @@ XpansionParsingError, ) from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import extract_filtering +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.model import ( Area, BindingConstraintDTO, @@ -44,7 +45,6 @@ FileStudyTreeConfig, Link, Simulation, - transform_name_to_id, ) from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( RenewableConfigType, diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py index e630f043da..b90fab4d47 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import re from typing import Any, Mapping, MutableMapping from pydantic import Field, model_validator @@ -45,7 +45,6 @@ def generate_id(cls, name: str) -> str: The ID of the section. """ # Avoid circular imports - from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id return transform_name_to_id(name, lower=False) @@ -98,6 +97,25 @@ def generate_id(cls, name: str) -> str: The ID of the section. """ # Avoid circular imports - from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id return transform_name_to_id(name, lower=True) + + +# Invalid chars was taken from Antares Simulator (C++). +_sub_invalid_chars = re.compile(r"[^a-zA-Z0-9_(),& -]+").sub + + +def transform_name_to_id(name: str, lower: bool = True) -> str: + """ + Transform a name into an identifier by replacing consecutive + invalid characters by a single white space, and then whitespaces + are striped from both ends. + + Valid characters are `[a-zA-Z0-9_(),& -]` (including space). + + Args: + name: The name to convert. + lower: The flag used to turn the identifier in lower case. + """ + valid_id = _sub_invalid_chars(" ", name).strip() + return valid_id.lower() if lower else valid_id diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/model.py b/antarest/study/storage/rawstudy/model/filesystem/config/model.py index abb307bba2..71e3f01837 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/model.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/model.py @@ -10,7 +10,6 @@ # # This file is part of the Antares project. -import re from pathlib import Path from typing import Any, Dict, List, MutableMapping, Optional, Set @@ -108,9 +107,10 @@ class DistrictSet(AntaresBaseModel): filters_year: List[str] = ALL def get_areas(self, all_areas: List[str]) -> List[str]: + areas = self.areas or [] if self.inverted_set: - return list(set(all_areas).difference(set(self.areas or []))) - return self.areas or [] + areas = list(set(all_areas).difference(set(areas))) + return sorted(areas) class Simulation(AntaresBaseModel): @@ -287,26 +287,6 @@ def get_filters_year(self, area: str, link: Optional[str] = None) -> List[str]: return self.areas[area].filters_year -# Invalid chars was taken from Antares Simulator (C++). -_sub_invalid_chars = re.compile(r"[^a-zA-Z0-9_(),& -]+").sub - - -def transform_name_to_id(name: str, lower: bool = True) -> str: - """ - Transform a name into an identifier by replacing consecutive - invalid characters by a single white space, and then whitespaces - are striped from both ends. - - Valid characters are `[a-zA-Z0-9_(),& -]` (including space). - - Args: - name: The name to convert. - lower: The flag used to turn the identifier in lower case. - """ - valid_id = _sub_invalid_chars(" ", name).strip() - return valid_id.lower() if lower else valid_id - - class FileStudyTreeConfigDTO(AntaresBaseModel): study_path: Path path: Path diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py b/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py index d8d5fc98a6..555ae996a6 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py @@ -19,7 +19,10 @@ from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.model import STUDY_VERSION_8_1 from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ClusterProperties -from antarest.study.storage.rawstudy.model.filesystem.config.identifier import IgnoreCaseIdentifier +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import ( + IgnoreCaseIdentifier, + transform_name_to_id, +) class TimeSeriesInterpretation(EnumIgnoreCase): @@ -45,15 +48,15 @@ class RenewableClusterGroup(EnumIgnoreCase): If not specified, the renewable cluster will be part of the group "Other RES 1". """ - THERMAL_SOLAR = "Solar Thermal" - PV_SOLAR = "Solar PV" - ROOFTOP_SOLAR = "Solar Rooftop" - WIND_ON_SHORE = "Wind Onshore" - WIND_OFF_SHORE = "Wind Offshore" - OTHER1 = "Other RES 1" - OTHER2 = "Other RES 2" - OTHER3 = "Other RES 3" - OTHER4 = "Other RES 4" + THERMAL_SOLAR = "solar thermal" + PV_SOLAR = "solar pv" + ROOFTOP_SOLAR = "solar rooftop" + WIND_ON_SHORE = "wind onshore" + WIND_OFF_SHORE = "wind offshore" + OTHER1 = "other res 1" + OTHER2 = "other res 2" + OTHER3 = "other res 3" + OTHER4 = "other res 4" @override def __repr__(self) -> str: @@ -68,7 +71,7 @@ def _missing_(cls, value: object) -> Optional["RenewableClusterGroup"]: if isinstance(value, str): # Check if any group value matches the input value ignoring case sensitivity. # noinspection PyUnresolvedReferences - if any(value.upper() == group.value.upper() for group in cls): + if any(value.lower() == group.value for group in cls): return cast(RenewableClusterGroup, super()._missing_(value)) # If a group is not found, return the default group ('OTHER1' by default). return cls.OTHER1 @@ -80,6 +83,11 @@ class RenewableProperties(ClusterProperties): Properties of a renewable cluster read from the configuration files. """ + # as a method, to avoid ambiguity with config subclass which has it + # as a property, which can differ from the name ... TODO: change this + def get_id(self) -> str: + return transform_name_to_id(self.name, lower=False) + group: RenewableClusterGroup = Field( title="Renewable Cluster Group", default=RenewableClusterGroup.OTHER1, @@ -113,6 +121,7 @@ class RenewableConfig(RenewableProperties, IgnoreCaseIdentifier): RenewableConfigType = RenewableConfig +RenewablePropertiesType = RenewableProperties def get_renewable_config_cls(study_version: StudyVersion) -> Type[RenewableConfig]: @@ -130,6 +139,25 @@ def get_renewable_config_cls(study_version: StudyVersion) -> Type[RenewableConfi raise ValueError(f"Unsupported study version {study_version}, required 810 or above.") +def create_renewable_properties(study_version: StudyVersion, data: Any) -> RenewablePropertiesType: + """ + Factory method to create renewable properties. + + Args: + study_version: The version of the study. + data: The properties to be used to initialize the model. + + Returns: + The renewable properties. + + Raises: + ValueError: If the study version is not supported. + """ + if study_version >= STUDY_VERSION_8_1: + return RenewableProperties.model_validate(data) + raise ValueError(f"Unsupported study version {study_version}, required 810 or above.") + + def create_renewable_config(study_version: StudyVersion, **kwargs: Any) -> RenewableConfigType: """ Factory method to create a renewable configuration model. diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py b/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py index de14d40d5d..6d4635b4ac 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py @@ -10,7 +10,7 @@ # # This file is part of the Antares project. -from typing import Any, Type +from typing import Any, Dict, Type, Union from antares.study.version import StudyVersion from pydantic import Field @@ -18,7 +18,7 @@ from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.model import STUDY_VERSION_8_6, STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ItemProperties -from antarest.study.storage.rawstudy.model.filesystem.config.identifier import LowerCaseIdentifier +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import LowerCaseIdentifier, transform_name_to_id class STStorageGroup(EnumIgnoreCase): @@ -34,15 +34,15 @@ class STStorageGroup(EnumIgnoreCase): - OTHER1...OTHER5: Represents other energy storage systems. """ - PSP_OPEN = "PSP_open" - PSP_CLOSED = "PSP_closed" - PONDAGE = "Pondage" - BATTERY = "Battery" - OTHER1 = "Other1" - OTHER2 = "Other2" - OTHER3 = "Other3" - OTHER4 = "Other4" - OTHER5 = "Other5" + PSP_OPEN = "psp_open" + PSP_CLOSED = "psp_closed" + PONDAGE = "pondage" + BATTERY = "battery" + OTHER1 = "other1" + OTHER2 = "other2" + OTHER3 = "other3" + OTHER4 = "other4" + OTHER5 = "other5" # noinspection SpellCheckingInspection @@ -53,6 +53,9 @@ class STStorageProperties(ItemProperties): All aliases match the name of the corresponding field in the INI files. """ + def get_id(self) -> str: + return transform_name_to_id(self.name) + group: STStorageGroup = Field( STStorageGroup.OTHER1, description="Energy storage system group", @@ -161,6 +164,28 @@ class STStorage880Config(STStorage880Properties, LowerCaseIdentifier): # NOTE: In the following Union, it is important to place the older version first, # because otherwise, creating a short term storage always creates a v8.8 one. STStorageConfigType = STStorageConfig | STStorage880Config +STStoragePropertiesType = STStorageProperties | STStorage880Properties + + +def create_st_storage_properties(study_version: StudyVersion, data: Dict[str, Any]) -> STStoragePropertiesType: + """ + Factory method to create st_storage properties. + + Args: + study_version: The version of the study. + data: The dictionary of data to be used to initialize the model. + + Returns: + The short term storage properties. + + Raises: + ValueError: If the study version is not supported. + """ + if study_version >= STUDY_VERSION_8_8: + return STStorage880Properties.model_validate(data) + elif study_version >= STUDY_VERSION_8_6: + return STStorageProperties.model_validate(data) + raise ValueError(f"Unsupported study version: {study_version}") def get_st_storage_config_cls(study_version: StudyVersion) -> Type[STStorageConfigType]: diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py index 829877457b..3e4be3ac41 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py @@ -10,7 +10,7 @@ # # This file is part of the Antares project. -from typing import Any, Optional, Type, cast +from typing import Any, Dict, Optional, Type, Union, cast from antares.study.version import StudyVersion from pydantic import Field @@ -18,7 +18,10 @@ from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.storage.rawstudy.model.filesystem.config.cluster import ClusterProperties -from antarest.study.storage.rawstudy.model.filesystem.config.identifier import IgnoreCaseIdentifier +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import ( + IgnoreCaseIdentifier, + transform_name_to_id, +) class LocalTSGenerationBehavior(EnumIgnoreCase): @@ -61,16 +64,16 @@ class ThermalClusterGroup(EnumIgnoreCase): The group `OTHER1` is used by default. """ - NUCLEAR = "Nuclear" - LIGNITE = "Lignite" - HARD_COAL = "Hard Coal" - GAS = "Gas" - OIL = "Oil" - MIXED_FUEL = "Mixed Fuel" - OTHER1 = "Other 1" - OTHER2 = "Other 2" - OTHER3 = "Other 3" - OTHER4 = "Other 4" + NUCLEAR = "nuclear" + LIGNITE = "lignite" + HARD_COAL = "hard coal" + GAS = "gas" + OIL = "oil" + MIXED_FUEL = "mixed fuel" + OTHER1 = "other 1" + OTHER2 = "other 2" + OTHER3 = "other 3" + OTHER4 = "other 4" @override def __repr__(self) -> str: # pragma: no cover @@ -85,10 +88,8 @@ def _missing_(cls, value: object) -> Optional["ThermalClusterGroup"]: if isinstance(value, str): # Check if any group value matches the input value ignoring case sensitivity. # noinspection PyUnresolvedReferences - if any(value.upper() == group.value.upper() for group in cls): + if any(value.lower() == group.value for group in cls): return cast(ThermalClusterGroup, super()._missing_(value)) - # If a group is not found, return the default group ('OTHER1' by default). - # Note that 'OTHER' is an alias for 'OTHER1'. return cls.OTHER1 return cast(Optional["ThermalClusterGroup"], super()._missing_(value)) @@ -109,6 +110,9 @@ class ThermalProperties(ClusterProperties): This model describes the configuration parameters for a thermal cluster. """ + def get_id(self) -> str: + return transform_name_to_id(self.name, lower=False) + group: ThermalClusterGroup = Field( default=ThermalClusterGroup.OTHER1, description="Thermal Cluster Group", @@ -409,6 +413,7 @@ class Thermal870Config(Thermal870Properties, IgnoreCaseIdentifier): # NOTE: In the following Union, it is important to place the most specific type first, # because the type matching generally occurs sequentially from left to right within the union. ThermalConfigType = Thermal870Config | Thermal860Config | ThermalConfig +ThermalPropertiesType = Thermal870Properties | Thermal860Properties | ThermalProperties def get_thermal_config_cls(study_version: StudyVersion) -> Type[ThermalConfigType]: @@ -429,6 +434,28 @@ def get_thermal_config_cls(study_version: StudyVersion) -> Type[ThermalConfigTyp return ThermalConfig +def create_thermal_properties(study_version: StudyVersion, data: Dict[str, Any]) -> ThermalPropertiesType: + """ + Factory method to create thermal properties. + + Args: + study_version: The version of the study. + data: The properties to be used to initialize the model. + + Returns: + The thermal properties. + + Raises: + ValueError: If the study version is not supported. + """ + if study_version >= 870: + return Thermal870Properties.model_validate(data) + elif study_version == 860: + return Thermal860Properties.model_validate(data) + else: + return ThermalProperties.model_validate(data) + + def create_thermal_config(study_version: StudyVersion, **kwargs: Any) -> ThermalConfigType: """ Factory method to create a thermal configuration model. diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/bindingconstraints/bindingconstraints_ini.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/bindingconstraints/bindingconstraints_ini.py index 49cfd81847..9067f734de 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/bindingconstraints/bindingconstraints_ini.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/bindingconstraints/bindingconstraints_ini.py @@ -9,11 +9,16 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from antarest.core.serde.ini_common import any_section_option_matcher +from antarest.core.serde.ini_reader import LOWER_CASE_PARSER, IniReader +from antarest.core.serde.ini_writer import LOWER_CASE_SERIALIZER, IniWriter from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode +_VALUE_PARSERS = {any_section_option_matcher("group"): LOWER_CASE_PARSER} +_VALUE_SERIALIZERS = {any_section_option_matcher("group"): LOWER_CASE_SERIALIZER} + # noinspection SpellCheckingInspection class BindingConstraintsIni(IniFileNode): @@ -34,4 +39,10 @@ class BindingConstraintsIni(IniFileNode): """ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): - super().__init__(context, config, types={}) + super().__init__( + context, + config, + types={}, + reader=IniReader(value_parsers=_VALUE_PARSERS), + writer=IniWriter(value_serializers=_VALUE_SERIALIZERS), + ) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py index 8db26ea89e..ebad9bb3cd 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/renewables/clusters.py @@ -11,12 +11,18 @@ # This file is part of the Antares project. from typing_extensions import override +from antarest.core.serde.ini_common import any_section_option_matcher +from antarest.core.serde.ini_reader import LOWER_CASE_PARSER, IniReader +from antarest.core.serde.ini_writer import LOWER_CASE_SERIALIZER, IniWriter from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode from antarest.study.storage.rawstudy.model.filesystem.inode import TREE +_VALUE_PARSERS = {any_section_option_matcher("group"): LOWER_CASE_PARSER} +_VALUE_SERIALIZERS = {any_section_option_matcher("group"): LOWER_CASE_SERIALIZER} + class ClusteredRenewableClusterConfig(IniFileNode): def __init__( @@ -30,11 +36,18 @@ def __init__( "group": str, "enabled": bool, "unitcount": int, - "nomialcapacity": 0, + "nominalcapacity": float, "ts-interpretation": str, } types = {cluster_id: section for cluster_id in config.get_renewable_ids(area)} - IniFileNode.__init__(self, context, config, types) + IniFileNode.__init__( + self, + context, + config, + types, + reader=IniReader(value_parsers=_VALUE_PARSERS), + writer=IniWriter(value_serializers=_VALUE_SERIALIZERS), + ) class ClusteredRenewableCluster(FolderNode): diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py index effd35ff88..49fbdf339a 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py @@ -9,11 +9,33 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from antares.study.version import StudyVersion +from antarest.core.serde.ini_common import any_section_option_matcher +from antarest.core.serde.ini_reader import LOWER_CASE_PARSER, IniReader +from antarest.core.serde.ini_writer import LOWER_CASE_SERIALIZER, IniWriter, ValueSerializer +from antarest.study.model import STUDY_VERSION_8_6 from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode +_VALUE_PARSERS = {any_section_option_matcher("group"): LOWER_CASE_PARSER} + + +def _write_group_8_6(input: str) -> str: + """ + The solver was not case insensitive to group, before version 8.6. + We need to write it with a capital first letter. + """ + return input.title() + + +def _get_group_serializer(study_version: StudyVersion) -> ValueSerializer: + if study_version <= STUDY_VERSION_8_6: + return _write_group_8_6 + else: + return LOWER_CASE_SERIALIZER + class InputSTStorageAreaList(IniFileNode): def __init__( @@ -33,4 +55,11 @@ def __init__( # - a withdrawal nominal capacity (double > 0) # - an injection nominal capacity (double > 0) types = {st_storage_id: dict for st_storage_id in config.get_st_storage_ids(area)} - super().__init__(context, config, types) + value_serializers = {any_section_option_matcher("group"): _get_group_serializer(config.version)} + super().__init__( + context, + config, + types, + reader=IniReader(value_parsers=_VALUE_PARSERS), + writer=IniWriter(value_serializers=value_serializers), + ) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py index adacbb8ffc..27e005529d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/cluster/area/list.py @@ -9,11 +9,16 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from antarest.core.serde.ini_common import any_section_option_matcher +from antarest.core.serde.ini_reader import LOWER_CASE_PARSER, IniReader +from antarest.core.serde.ini_writer import LOWER_CASE_SERIALIZER, IniWriter from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode +_VALUE_PARSERS = {any_section_option_matcher("group"): LOWER_CASE_PARSER} +_VALUE_SERIALIZERS = {any_section_option_matcher("group"): LOWER_CASE_SERIALIZER} + class InputThermalClustersAreaList(IniFileNode): def __init__( @@ -30,4 +35,10 @@ def __init__( "market-bid-cost": float, } types = {th: section for th in config.get_thermal_ids(area)} - super().__init__(context, config, types) + super().__init__( + context, + config, + types, + reader=IniReader(value_parsers=_VALUE_PARSERS), + writer=IniWriter(value_serializers=_VALUE_SERIALIZERS), + ) diff --git a/antarest/study/storage/variantstudy/business/utils.py b/antarest/study/storage/variantstudy/business/utils.py index d94f6fe0c5..25f2963b25 100644 --- a/antarest/study/storage/variantstudy/business/utils.py +++ b/antarest/study/storage/variantstudy/business/utils.py @@ -103,38 +103,58 @@ def decode(alias: str, study: FileStudy) -> str: def transform_command_to_dto( commands: Sequence[ICommand], - ref_commands: Optional[Sequence[CommandDTO]] = None, + ref_command_dtos: Optional[Sequence[CommandDTO]] = None, force_aggregate: bool = False, ) -> List[CommandDTO]: + """ + Converts the list of input commands to DTOs. + + Since DTOs can contain the arguments for multiple commands: + - if ref_commands is provided, we keep the same aggregation + - if force_aggregate is True, we aggregate all we can + + # TODO: the implementation is a mess, and actually the 2 additional + # arguments are mutually exclusive, we should separate in 2 methods. + """ if len(commands) <= 1: return [command.to_dto() for command in commands] commands_dto: List[CommandDTO] = [] - ref_commands_dto = ref_commands if ref_commands is not None else [command.to_dto() for command in commands] + ref_commands_dto = ref_command_dtos if ref_command_dtos is not None else [command.to_dto() for command in commands] prev_command = commands[0] cur_dto_index = 0 - cur_dto = ref_commands_dto[cur_dto_index] - cur_dto_arg_count = 1 if isinstance(cur_dto.args, dict) else len(cur_dto.args) - cur_command_args_batch = [prev_command.to_dto().args] + ref_dto = ref_commands_dto[cur_dto_index] + cur_dto_arg_count = len(ref_dto.get_args_list()) + new_dto = prev_command.to_dto() + cur_command_version = new_dto.version + cur_command_args_batch = [new_dto.args] for command in commands[1:]: cur_dto_arg_count -= 1 if command.command_name == prev_command.command_name and (cur_dto_arg_count > 0 or force_aggregate): - cur_command_args_batch.append(command.to_dto().args) + new_dto = command.to_dto() + if new_dto.version != cur_command_version: + raise ValueError("Aggregated commands cannot have different versions.") + cur_command_args_batch.append(new_dto.args) else: commands_dto.append( CommandDTO( action=prev_command.command_name.value, + version=cur_command_version, args=cur_command_args_batch, study_version=prev_command.study_version, ) ) - cur_command_args_batch = [command.to_dto().args] + new_dto = command.to_dto() + cur_command_version = new_dto.version + cur_command_args_batch = [new_dto.args] + cur_dto_index += 1 - cur_dto = ref_commands_dto[cur_dto_index] - cur_dto_arg_count = 1 if isinstance(cur_dto.args, dict) else len(cur_dto.args) + ref_dto = ref_commands_dto[cur_dto_index] + cur_dto_arg_count = len(ref_dto.get_args_list()) prev_command = command commands_dto.append( CommandDTO( action=prev_command.command_name.value, + version=cur_command_version, args=cur_command_args_batch, study_version=prev_command.study_version, ) diff --git a/antarest/study/storage/variantstudy/command_factory.py b/antarest/study/storage/variantstudy/command_factory.py index a9956a86a2..8f9e861885 100644 --- a/antarest/study/storage/variantstudy/command_factory.py +++ b/antarest/study/storage/variantstudy/command_factory.py @@ -11,7 +11,8 @@ # This file is part of the Antares project. import copy -from typing import List, Optional +from dataclasses import dataclass +from typing import Dict, List, Optional, Type from antares.study.version import StudyVersion @@ -56,7 +57,7 @@ from antarest.study.storage.variantstudy.model.command_context import CommandContext from antarest.study.storage.variantstudy.model.model import CommandDTO -COMMAND_MAPPING = { +COMMAND_MAPPING: Dict[str, Type[ICommand]] = { CommandName.CREATE_AREA.value: CreateArea, CommandName.UPDATE_AREA_UI.value: UpdateAreaUI, CommandName.REMOVE_AREA.value: RemoveArea, @@ -88,6 +89,11 @@ } +@dataclass(frozen=True) +class CommandValidationContext: + version: int + + class CommandFactory: """ Service to convert CommendDTO to Command @@ -111,13 +117,15 @@ def _to_single_command( """Convert a single CommandDTO to ICommand.""" if action in COMMAND_MAPPING: command_class = COMMAND_MAPPING[action] - return command_class( # type: ignore - **args, - command_context=self.command_context, - version=version, - command_id=command_id, - study_version=study_version, + data = copy.deepcopy(args) + data.update( + { + "command_context": self.command_context, + "command_id": command_id, + "study_version": study_version, + } ) + return command_class.model_validate(data, context=CommandValidationContext(version=version)) raise NotImplementedError(action) def to_command(self, command_dto: CommandDTO) -> List[ICommand]: diff --git a/antarest/study/storage/variantstudy/model/command/create_area.py b/antarest/study/storage/variantstudy/model/command/create_area.py index c7b0f81497..5b1de89a7c 100644 --- a/antarest/study/storage/variantstudy/model/command/create_area.py +++ b/antarest/study/storage/variantstudy/model/command/create_area.py @@ -17,12 +17,8 @@ from antarest.core.model import JSON from antarest.study.model import STUDY_VERSION_6_5, STUDY_VERSION_8_1, STUDY_VERSION_8_3, STUDY_VERSION_8_6 -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - Area, - EnrModelling, - FileStudyTreeConfig, - transform_name_to_id, -) +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, EnrModelling, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput, FilteringOptions from antarest.study.storage.variantstudy.model.command.icommand import ICommand @@ -65,7 +61,6 @@ class CreateArea(ICommand): # =================== command_name: CommandName = CommandName.CREATE_AREA - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py index a0bfdcb503..04a659791d 100644 --- a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py @@ -12,13 +12,14 @@ from abc import ABCMeta from enum import Enum -from typing import Any, Dict, List, Optional, Set, Tuple, Type +from typing import Any, Dict, Final, List, Optional, Set, Tuple, Type import numpy as np from antares.study.version import StudyVersion from pydantic import Field, field_validator, model_validator from typing_extensions import override +from antarest.core.model import LowerCaseStr from antarest.core.serde import AntaresBaseModel from antarest.matrixstore.model import MatrixData from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model @@ -31,7 +32,8 @@ BindingConstraintOperator, ) from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import validate_filtering -from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig, transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants from antarest.study.storage.variantstudy.business.utils import validate_matrix @@ -119,7 +121,7 @@ def _validate_filtering(cls, v: Any) -> str: class BindingConstraintProperties870(BindingConstraintProperties830): - group: str = DEFAULT_GROUP + group: LowerCaseStr = DEFAULT_GROUP BindingConstraintProperties = ( @@ -213,6 +215,8 @@ class AbstractBindingConstraintCommand(OptionalProperties, BindingConstraintMatr Abstract class for binding constraint commands. """ + _SERIALIZATION_VERSION: Final[int] = 1 + coeffs: Optional[Dict[str, List[float]]] = None @override @@ -240,7 +244,10 @@ def to_dto(self) -> CommandDTO: args[matrix_name] = matrix_service.get_matrix_id(matrix_attr) return CommandDTO( - action=self.command_name.value, args=args, version=self.version, study_version=self.study_version + action=self.command_name.value, + args=args, + version=self._SERIALIZATION_VERSION, + study_version=self.study_version, ) @override diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py index 82b5fbe840..032c10e34b 100644 --- a/antarest/study/storage/variantstudy/model/command/create_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py @@ -9,22 +9,24 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import typing as t +from typing import Any, ClassVar, Dict, Final, List, Optional, Tuple -from typing import Any, Dict, List, Optional, Tuple - -from pydantic import Field, ValidationInfo, field_validator +from pydantic import Field, model_validator +from pydantic_core.core_schema import ValidationInfo from typing_extensions import override from antarest.core.model import JSON from antarest.core.utils.utils import assert_this from antarest.matrixstore.model import MatrixData from antarest.study.model import STUDY_VERSION_8_7 -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - Area, - FileStudyTreeConfig, - transform_name_to_id, +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import AreaId +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( + ThermalPropertiesType, + create_thermal_config, + create_thermal_properties, ) -from antarest.study.storage.rawstudy.model.filesystem.config.thermal import create_thermal_config from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol, validate_matrix from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput @@ -32,6 +34,8 @@ from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener from antarest.study.storage.variantstudy.model.model import CommandDTO +OptionalMatrixData = List[List[MatrixData]] | str | None + class CreateCluster(ICommand): """ @@ -42,50 +46,45 @@ class CreateCluster(ICommand): # =================== command_name: CommandName = CommandName.CREATE_THERMAL_CLUSTER - version: int = 1 # Command parameters # ================== - area_id: str - cluster_name: str - parameters: Dict[str, Any] - prepro: Optional[List[List[MatrixData]] | str] = Field(None, validate_default=True) - modulation: Optional[List[List[MatrixData]] | str] = Field(None, validate_default=True) - - @field_validator("cluster_name", mode="before") - def validate_cluster_name(cls, val: str) -> str: - valid_name = transform_name_to_id(val, lower=False) - if valid_name != val: - raise ValueError("Cluster name must only contains [a-zA-Z0-9],&,-,_,(,) characters") - return val - - @field_validator("prepro", mode="before") - def validate_prepro( - cls, - v: Optional[List[List[MatrixData]] | str], - values: Dict[str, Any] | ValidationInfo, - ) -> Optional[List[List[MatrixData]] | str]: - new_values = values if isinstance(values, dict) else values.data - if v is None: - v = new_values["command_context"].generator_matrix_constants.get_thermal_prepro_data() - return v + area_id: AreaId + parameters: ThermalPropertiesType + prepro: OptionalMatrixData = Field(None, validate_default=True) + modulation: OptionalMatrixData = Field(None, validate_default=True) + + # version 2: remove cluster_name and type parameters as ThermalPropertiesType + _SERIALIZATION_VERSION: Final[int] = 2 + + @property + def cluster_name(self) -> str: + return self.parameters.name + + @model_validator(mode="before") + @classmethod + def validate_model(cls, values: Dict[str, t.Any], info: ValidationInfo) -> Dict[str, Any]: + # Validate parameters + if isinstance(values["parameters"], dict): + parameters = values["parameters"] + if info.context and info.context.version == 1: + parameters["name"] = values.pop("cluster_name") + values["parameters"] = create_thermal_properties(values["study_version"], parameters) + + # Validate prepro + if "prepro" in values: + values["prepro"] = validate_matrix(values["prepro"], values) else: - return validate_matrix(v, new_values) - - @field_validator("modulation", mode="before") - def validate_modulation( - cls, - v: Optional[List[List[MatrixData]] | str], - values: Dict[str, Any] | ValidationInfo, - ) -> Optional[List[List[MatrixData]] | str]: - new_values = values if isinstance(values, dict) else values.data - if v is None: - v = new_values["command_context"].generator_matrix_constants.get_thermal_prepro_modulation() - return v + values["prepro"] = values["command_context"].generator_matrix_constants.get_thermal_prepro_data() + # Validate modulation + if "modulation" in values: + values["modulation"] = validate_matrix(values["modulation"], values) else: - return validate_matrix(v, new_values) + values["modulation"] = values["command_context"].generator_matrix_constants.get_thermal_prepro_modulation() + + return values @override def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: @@ -128,12 +127,11 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N if not output.status: return output - # default values - self.parameters.setdefault("name", self.cluster_name) + version = study_data.config.version cluster_id = data["cluster_id"] config = study_data.tree.get(["input", "thermal", "clusters", self.area_id, "list"]) - config[cluster_id] = self.parameters + config[cluster_id] = self.parameters.model_dump(mode="json", by_alias=True) # Series identifiers are in lower case. series_id = cluster_id.lower() @@ -154,7 +152,7 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N } } } - if study_data.config.version >= STUDY_VERSION_8_7: + if version >= STUDY_VERSION_8_7: new_cluster_data["input"]["thermal"]["series"][self.area_id][series_id]["CO2Cost"] = null_matrix new_cluster_data["input"]["thermal"]["series"][self.area_id][series_id]["fuelCost"] = null_matrix study_data.tree.save(new_cluster_data) @@ -164,11 +162,11 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N @override def to_dto(self) -> CommandDTO: return CommandDTO( + version=self._SERIALIZATION_VERSION, action=self.command_name.value, args={ "area_id": self.area_id, - "cluster_name": self.cluster_name, - "parameters": self.parameters, + "parameters": self.parameters.model_dump(mode="json", by_alias=True), "prepro": strip_matrix_protocol(self.prepro), "modulation": strip_matrix_protocol(self.modulation), }, diff --git a/antarest/study/storage/variantstudy/model/command/create_district.py b/antarest/study/storage/variantstudy/model/command/create_district.py index 7307443833..7aa76754a0 100644 --- a/antarest/study/storage/variantstudy/model/command/create_district.py +++ b/antarest/study/storage/variantstudy/model/command/create_district.py @@ -16,11 +16,8 @@ from pydantic import field_validator from typing_extensions import override -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - DistrictSet, - FileStudyTreeConfig, - transform_name_to_id, -) +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import DistrictSet, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput from antarest.study.storage.variantstudy.model.command.icommand import ICommand @@ -42,7 +39,6 @@ class CreateDistrict(ICommand): # =================== command_name: CommandName = CommandName.CREATE_DISTRICT - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py index f4b32c9943..b009c6a39e 100644 --- a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py @@ -10,19 +10,19 @@ # # This file is part of the Antares project. -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, Final, List, Optional, Tuple -from pydantic import field_validator +from pydantic import ValidationInfo, model_validator from typing_extensions import override from antarest.core.model import JSON -from antarest.study.storage.rawstudy.model.filesystem.config.model import ( - Area, - EnrModelling, - FileStudyTreeConfig, - transform_name_to_id, +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import AreaId +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, EnrModelling, FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( + RenewableProperties, + create_renewable_config, + create_renewable_properties, ) -from antarest.study.storage.rawstudy.model.filesystem.config.renewable import create_renewable_config from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput from antarest.study.storage.variantstudy.model.command.icommand import ICommand @@ -39,21 +39,30 @@ class CreateRenewablesCluster(ICommand): # =================== command_name: CommandName = CommandName.CREATE_RENEWABLES_CLUSTER - version: int = 1 + + # version 2: remove cluster_name and type parameters as RenewableProperties + _SERIALIZATION_VERSION: Final[int] = 2 # Command parameters # ================== - area_id: str - cluster_name: str - parameters: Dict[str, Any] + area_id: AreaId + parameters: RenewableProperties + + @property + def cluster_name(self) -> str: + return self.parameters.name - @field_validator("cluster_name") - def validate_cluster_name(cls, val: str) -> str: - valid_name = transform_name_to_id(val, lower=False) - if valid_name != val: - raise ValueError("Area name must only contains [a-zA-Z0-9],&,-,_,(,) characters") - return val + @model_validator(mode="before") + @classmethod + def validate_model(cls, values: Dict[str, Any], info: ValidationInfo) -> Dict[str, Any]: + # Validate parameters + if isinstance(values["parameters"], dict): + parameters = values["parameters"] + if info.context and info.context.version == 1: + parameters["name"] = values.pop("cluster_name") + values["parameters"] = create_renewable_properties(values["study_version"], parameters) + return values @override def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: @@ -109,14 +118,9 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N if not output.status: return output - # default values - if "ts-interpretation" not in self.parameters: - self.parameters["ts-interpretation"] = "power-generation" - self.parameters.setdefault("name", self.cluster_name) - cluster_id = data["cluster_id"] config = study_data.tree.get(["input", "renewables", "clusters", self.area_id, "list"]) - config[cluster_id] = self.parameters + config[cluster_id] = self.parameters.model_dump(mode="json", by_alias=True) # Series identifiers are in lower case. series_id = cluster_id.lower() @@ -140,10 +144,10 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N def to_dto(self) -> CommandDTO: return CommandDTO( action=self.command_name.value, + version=self._SERIALIZATION_VERSION, args={ "area_id": self.area_id, - "cluster_name": self.cluster_name, - "parameters": self.parameters, + "parameters": self.parameters.model_dump(mode="json", by_alias=True), }, study_version=self.study_version, ) diff --git a/antarest/study/storage/variantstudy/model/command/create_st_storage.py b/antarest/study/storage/variantstudy/model/command/create_st_storage.py index 7d1d8da4e8..43723bd5a9 100644 --- a/antarest/study/storage/variantstudy/model/command/create_st_storage.py +++ b/antarest/study/storage/variantstudy/model/command/create_st_storage.py @@ -10,7 +10,8 @@ # # This file is part of the Antares project. -from typing import Any, Dict, List, Optional, Tuple, cast + +from typing import Any, Dict, Final, List, Optional, Tuple, Union, cast import numpy as np from pydantic import Field, ValidationInfo, model_validator @@ -19,8 +20,14 @@ from antarest.core.model import JSON from antarest.matrixstore.model import MatrixData from antarest.study.model import STUDY_VERSION_8_6 +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import AreaId +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfigType +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( + STStoragePropertiesType, + create_st_storage_config, + create_st_storage_properties, +) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol, validate_matrix @@ -54,14 +61,16 @@ class CreateSTStorage(ICommand): # =================== command_name: CommandName = CommandName.CREATE_ST_STORAGE - version: int = 1 + + # version 2: remove cluster_name and type parameters as STStoragePropertiesType + _SERIALIZATION_VERSION: Final[int] = 2 # Command parameters # ================== - area_id: str = Field(description="Area ID", pattern=r"[a-z0-9_(),& -]+") - parameters: STStorageConfigType - pmax_injection: Optional[MatrixType | str] = Field( + area_id: AreaId + parameters: STStoragePropertiesType + pmax_injection: Optional[Union[MatrixType, str]] = Field( default=None, description="Charge capacity (modulation)", ) @@ -85,13 +94,23 @@ class CreateSTStorage(ICommand): @property def storage_id(self) -> str: """The normalized version of the storage's name used as the ID.""" - return self.parameters.id + return transform_name_to_id(self.storage_name) @property def storage_name(self) -> str: """The label representing the name of the storage for the user.""" return self.parameters.name + @model_validator(mode="before") + @classmethod + def validate_model(cls, values: Dict[str, Any], info: ValidationInfo) -> Dict[str, Any]: + if isinstance(values["parameters"], dict): + parameters = values["parameters"] + if info.context and info.context.version == 1: + parameters["name"] = values.pop("cluster_name") + values["parameters"] = create_st_storage_properties(values["study_version"], parameters) + return values + @staticmethod def validate_field(v: Optional[MatrixType | str], values: Dict[str, Any], field: str) -> Optional[MatrixType | str]: """ @@ -173,6 +192,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, """ # Check if the study version is above the minimum required version. + storage_id = self.storage_id version = study_data.version if version < REQUIRED_VERSION: return ( @@ -195,7 +215,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, area: Area = study_data.areas[self.area_id] # Check if the short-term storage already exists in the area - if any(s.id == self.storage_id for s in area.st_storages): + if any(s.id == storage_id for s in area.st_storages): return ( CommandOutput( status=False, @@ -205,14 +225,17 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, ) # Create a new short-term storage and add it to the area - area.st_storages.append(self.parameters) + storage_config = create_st_storage_config( + self.study_version, **self.parameters.model_dump(mode="json", by_alias=True) + ) + area.st_storages.append(storage_config) return ( CommandOutput( status=True, message=f"Short-term st_storage '{self.storage_name}' successfully added to area '{self.area_id}'.", ), - {"storage_id": self.storage_id}, + {"storage_id": storage_id}, ) @override @@ -228,6 +251,7 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N Returns: The output of the command execution. """ + storage_id = self.storage_id output, _ = self._apply_config(study_data.config) if not output.status: return output @@ -235,13 +259,13 @@ def _apply(self, study_data: FileStudy, listener: Optional[ICommandListener] = N # Fill-in the "list.ini" file with the parameters. # On creation, it's better to write all the parameters in the file. config = study_data.tree.get(["input", "st-storage", "clusters", self.area_id, "list"]) - config[self.storage_id] = self.parameters.model_dump(mode="json", by_alias=True, exclude={"id"}) + config[storage_id] = self.parameters.model_dump(mode="json", by_alias=True) new_data: JSON = { "input": { "st-storage": { "clusters": {self.area_id: {"list": config}}, - "series": {self.area_id: {self.storage_id: {attr: getattr(self, attr) for attr in _MATRIX_NAMES}}}, + "series": {self.area_id: {storage_id: {attr: getattr(self, attr) for attr in _MATRIX_NAMES}}}, } } } @@ -258,12 +282,12 @@ def to_dto(self) -> CommandDTO: Returns: The DTO object representing the current command. """ - parameters = self.parameters.model_dump(mode="json", by_alias=True, exclude={"id"}) return CommandDTO( action=self.command_name.value, + version=self._SERIALIZATION_VERSION, args={ "area_id": self.area_id, - "parameters": parameters, + "parameters": self.parameters.model_dump(mode="json", by_alias=True), **{attr: strip_matrix_protocol(getattr(self, attr)) for attr in _MATRIX_NAMES}, }, study_version=self.study_version, diff --git a/antarest/study/storage/variantstudy/model/command/create_user_resource.py b/antarest/study/storage/variantstudy/model/command/create_user_resource.py index 901599529d..b02bce5992 100644 --- a/antarest/study/storage/variantstudy/model/command/create_user_resource.py +++ b/antarest/study/storage/variantstudy/model/command/create_user_resource.py @@ -45,7 +45,6 @@ class CreateUserResource(ICommand): # =================== command_name: CommandName = CommandName.CREATE_USER_RESOURCE - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/generate_thermal_cluster_timeseries.py b/antarest/study/storage/variantstudy/model/command/generate_thermal_cluster_timeseries.py index eaf2bb73fc..26d223dd3c 100644 --- a/antarest/study/storage/variantstudy/model/command/generate_thermal_cluster_timeseries.py +++ b/antarest/study/storage/variantstudy/model/command/generate_thermal_cluster_timeseries.py @@ -46,7 +46,6 @@ class GenerateThermalClusterTimeSeries(ICommand): """ command_name: CommandName = CommandName.GENERATE_THERMAL_CLUSTER_TIMESERIES - version: int = 1 @override def _apply_config(self, study_data: FileStudyTreeConfig) -> OutputTuple: diff --git a/antarest/study/storage/variantstudy/model/command/icommand.py b/antarest/study/storage/variantstudy/model/command/icommand.py index 7152a90123..2420ba0bc7 100644 --- a/antarest/study/storage/variantstudy/model/command/icommand.py +++ b/antarest/study/storage/variantstudy/model/command/icommand.py @@ -40,13 +40,11 @@ class ICommand(ABC, AntaresBaseModel, extra="forbid", arbitrary_types_allowed=Tr Attributes: command_id: The ID of the command extracted from the database, if any. command_name: The name of the command. - version: The version of the command (currently always equal to 1). command_context: The context of the command. """ command_id: Optional[uuid.UUID] = None command_name: CommandName - version: int command_context: CommandContext study_version: StudyVersionStr diff --git a/antarest/study/storage/variantstudy/model/command/remove_area.py b/antarest/study/storage/variantstudy/model/command/remove_area.py index 2250943400..7c9fb55573 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_area.py +++ b/antarest/study/storage/variantstudy/model/command/remove_area.py @@ -44,7 +44,6 @@ class RemoveArea(ICommand): """ command_name: CommandName = CommandName.REMOVE_AREA - version: int = 1 # Properties of the `REMOVE_AREA` command: id: str diff --git a/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py index 4a4af0b214..99f4277e54 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/remove_binding_constraint.py @@ -32,7 +32,6 @@ class RemoveBindingConstraint(ICommand): """ command_name: CommandName = CommandName.REMOVE_BINDING_CONSTRAINT - version: int = 1 # Properties of the `REMOVE_BINDING_CONSTRAINT` command: id: str diff --git a/antarest/study/storage/variantstudy/model/command/remove_cluster.py b/antarest/study/storage/variantstudy/model/command/remove_cluster.py index 711d5bb0ff..ed689b7043 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/remove_cluster.py @@ -34,7 +34,6 @@ class RemoveCluster(ICommand): # =================== command_name: CommandName = CommandName.REMOVE_THERMAL_CLUSTER - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/remove_district.py b/antarest/study/storage/variantstudy/model/command/remove_district.py index 9d1cdeaec4..13ba726f65 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_district.py +++ b/antarest/study/storage/variantstudy/model/command/remove_district.py @@ -31,7 +31,6 @@ class RemoveDistrict(ICommand): # =================== command_name: CommandName = CommandName.REMOVE_DISTRICT - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/remove_link.py b/antarest/study/storage/variantstudy/model/command/remove_link.py index 93c0eb58f6..5a3266def6 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_link.py +++ b/antarest/study/storage/variantstudy/model/command/remove_link.py @@ -16,7 +16,8 @@ from typing_extensions import override from antarest.study.model import STUDY_VERSION_8_2 -from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig, transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput from antarest.study.storage.variantstudy.model.command.icommand import ICommand, OutputTuple @@ -33,7 +34,6 @@ class RemoveLink(ICommand): # =================== command_name: CommandName = CommandName.REMOVE_LINK - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py b/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py index b106568391..089223daee 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py +++ b/antarest/study/storage/variantstudy/model/command/remove_multiple_binding_constraints.py @@ -31,7 +31,6 @@ class RemoveMultipleBindingConstraints(ICommand): """ command_name: CommandName = CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS - version: int = 1 # Properties of the `REMOVE_MULTIPLE_BINDING_CONSTRAINTS` command: ids: List[str] diff --git a/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py b/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py index 5bee20d694..76771c2a99 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py @@ -31,7 +31,6 @@ class RemoveRenewablesCluster(ICommand): # =================== command_name: CommandName = CommandName.REMOVE_RENEWABLES_CLUSTER - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/remove_st_storage.py b/antarest/study/storage/variantstudy/model/command/remove_st_storage.py index 97c6edf0b2..8aa5b8ccc8 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_st_storage.py +++ b/antarest/study/storage/variantstudy/model/command/remove_st_storage.py @@ -36,7 +36,6 @@ class RemoveSTStorage(ICommand): # =================== command_name: CommandName = CommandName.REMOVE_ST_STORAGE - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/remove_user_resource.py b/antarest/study/storage/variantstudy/model/command/remove_user_resource.py index f90ea7b163..dca721b16b 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_user_resource.py +++ b/antarest/study/storage/variantstudy/model/command/remove_user_resource.py @@ -38,7 +38,6 @@ class RemoveUserResource(ICommand): # =================== command_name: CommandName = CommandName.REMOVE_USER_RESOURCE - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/replace_matrix.py b/antarest/study/storage/variantstudy/model/command/replace_matrix.py index f72c1ee1cf..181fe4027c 100644 --- a/antarest/study/storage/variantstudy/model/command/replace_matrix.py +++ b/antarest/study/storage/variantstudy/model/command/replace_matrix.py @@ -38,7 +38,6 @@ class ReplaceMatrix(ICommand): # =================== command_name: CommandName = CommandName.REPLACE_MATRIX - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_area_ui.py b/antarest/study/storage/variantstudy/model/command/update_area_ui.py index 6e158149e0..d395e3dea3 100644 --- a/antarest/study/storage/variantstudy/model/command/update_area_ui.py +++ b/antarest/study/storage/variantstudy/model/command/update_area_ui.py @@ -32,7 +32,6 @@ class UpdateAreaUI(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_AREA_UI - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py index 67b5a5b429..118836acd2 100644 --- a/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/update_binding_constraint.py @@ -106,7 +106,6 @@ class UpdateBindingConstraint(AbstractBindingConstraintCommand): # =================== command_name: CommandName = CommandName.UPDATE_BINDING_CONSTRAINT - version: int = 1 # Command parameters # ================== @@ -216,5 +215,5 @@ def to_dto(self) -> CommandDTO: json_command[key] = matrix_service.get_matrix_id(json_command[key]) return CommandDTO( - action=self.command_name.value, args=json_command, version=self.version, study_version=self.study_version + action=self.command_name.value, args=json_command, version=1, study_version=self.study_version ) diff --git a/antarest/study/storage/variantstudy/model/command/update_comments.py b/antarest/study/storage/variantstudy/model/command/update_comments.py index a73e3c36a6..b600dd7269 100644 --- a/antarest/study/storage/variantstudy/model/command/update_comments.py +++ b/antarest/study/storage/variantstudy/model/command/update_comments.py @@ -32,7 +32,6 @@ class UpdateComments(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_COMMENTS - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_config.py b/antarest/study/storage/variantstudy/model/command/update_config.py index ead39a4f68..831df4c7f4 100644 --- a/antarest/study/storage/variantstudy/model/command/update_config.py +++ b/antarest/study/storage/variantstudy/model/command/update_config.py @@ -47,7 +47,6 @@ class UpdateConfig(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_CONFIG - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_district.py b/antarest/study/storage/variantstudy/model/command/update_district.py index 224c921da5..6a9a0fd4b2 100644 --- a/antarest/study/storage/variantstudy/model/command/update_district.py +++ b/antarest/study/storage/variantstudy/model/command/update_district.py @@ -32,7 +32,6 @@ class UpdateDistrict(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_DISTRICT - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_link.py b/antarest/study/storage/variantstudy/model/command/update_link.py index 742ff54d4a..ff04db5f2d 100644 --- a/antarest/study/storage/variantstudy/model/command/update_link.py +++ b/antarest/study/storage/variantstudy/model/command/update_link.py @@ -32,7 +32,6 @@ class UpdateLink(AbstractLinkCommand): # =================== command_name: CommandName = CommandName.UPDATE_LINK - version: int = 1 @override def _apply_config(self, study_data: FileStudyTreeConfig) -> OutputTuple: diff --git a/antarest/study/storage/variantstudy/model/command/update_playlist.py b/antarest/study/storage/variantstudy/model/command/update_playlist.py index 89fcd985c7..c2e35b968e 100644 --- a/antarest/study/storage/variantstudy/model/command/update_playlist.py +++ b/antarest/study/storage/variantstudy/model/command/update_playlist.py @@ -32,7 +32,6 @@ class UpdatePlaylist(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_PLAYLIST - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_raw_file.py b/antarest/study/storage/variantstudy/model/command/update_raw_file.py index d124a728d4..e456e875b7 100644 --- a/antarest/study/storage/variantstudy/model/command/update_raw_file.py +++ b/antarest/study/storage/variantstudy/model/command/update_raw_file.py @@ -33,7 +33,6 @@ class UpdateRawFile(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_FILE - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py b/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py index b2c1ad9952..cd65d26b01 100644 --- a/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py +++ b/antarest/study/storage/variantstudy/model/command/update_scenario_builder.py @@ -48,7 +48,6 @@ class UpdateScenarioBuilder(ICommand): # =================== command_name: CommandName = CommandName.UPDATE_SCENARIO_BUILDER - version: int = 1 # Command parameters # ================== diff --git a/antarest/study/storage/variantstudy/model/model.py b/antarest/study/storage/variantstudy/model/model.py index d7d258994a..daa1ad6190 100644 --- a/antarest/study/storage/variantstudy/model/model.py +++ b/antarest/study/storage/variantstudy/model/model.py @@ -11,7 +11,7 @@ # This file is part of the Antares project. import datetime import uuid -from typing import MutableSequence, Optional, Tuple +from typing import List, MutableSequence, Optional, Tuple import typing_extensions as te @@ -93,7 +93,7 @@ class CommandDTO(AntaresBaseModel): id: Optional[str] = None action: str - args: MutableSequence[JSON] | JSON + args: List[JSON] | JSON version: int = 1 study_version: StudyVersionStr user_id: Optional[int] = None @@ -104,6 +104,9 @@ def to_api(self, user_name: Optional[str] = None) -> CommandDTOAPI: data["user_name"] = user_name return CommandDTOAPI.model_validate(data) + def get_args_list(self) -> MutableSequence[JSON]: + return self.args if isinstance(self.args, list) else [self.args] + class CommandResultDTO(AntaresBaseModel): """ diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index befee9efcd..da8a500366 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -81,7 +81,7 @@ BindingConstraintFrequency, BindingConstraintOperator, ) -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.ruleset_matrices import TableForm as SBTableForm logger = logging.getLogger(__name__) diff --git a/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json b/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json index c0d5635c70..a164d8a163 100644 --- a/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json +++ b/tests/integration/studies_blueprint/assets/test_synthesis/raw_study.synthesis.json @@ -18,7 +18,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -42,7 +42,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -66,7 +66,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -90,7 +90,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -114,7 +114,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -138,7 +138,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -162,7 +162,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -186,7 +186,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -210,7 +210,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, @@ -258,7 +258,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -282,7 +282,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -306,7 +306,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -330,7 +330,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -354,7 +354,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -378,7 +378,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -402,7 +402,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -426,7 +426,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -450,7 +450,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, @@ -498,7 +498,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -522,7 +522,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -546,7 +546,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -570,7 +570,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -594,7 +594,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -618,7 +618,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -642,7 +642,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -666,7 +666,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -690,7 +690,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, @@ -726,7 +726,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -750,7 +750,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -774,7 +774,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -798,7 +798,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -822,7 +822,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -846,7 +846,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -870,7 +870,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -894,7 +894,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -918,7 +918,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, diff --git a/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json b/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json index 2d3c3baf2a..f16fba25eb 100644 --- a/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json +++ b/tests/integration/studies_blueprint/assets/test_synthesis/variant_study.synthesis.json @@ -18,7 +18,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -42,7 +42,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -66,7 +66,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -90,7 +90,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -114,7 +114,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -138,7 +138,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -162,7 +162,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -186,7 +186,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -210,7 +210,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, @@ -258,7 +258,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -282,7 +282,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -306,7 +306,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -330,7 +330,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -354,7 +354,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -378,7 +378,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -402,7 +402,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -426,7 +426,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -450,7 +450,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, @@ -498,7 +498,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -522,7 +522,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -546,7 +546,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -570,7 +570,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -594,7 +594,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -618,7 +618,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -642,7 +642,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -666,7 +666,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -690,7 +690,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, @@ -726,7 +726,7 @@ "thermals": [ { "id": "01_solar", - "group": "Other 1", + "group": "other 1", "name": "01_solar", "enabled": true, "unitcount": 1, @@ -750,7 +750,7 @@ }, { "id": "02_wind_on", - "group": "Other 1", + "group": "other 1", "name": "02_wind_on", "enabled": true, "unitcount": 1, @@ -774,7 +774,7 @@ }, { "id": "03_wind_off", - "group": "Other 1", + "group": "other 1", "name": "03_wind_off", "enabled": true, "unitcount": 1, @@ -798,7 +798,7 @@ }, { "id": "04_res", - "group": "Other 1", + "group": "other 1", "name": "04_res", "enabled": true, "unitcount": 1, @@ -822,7 +822,7 @@ }, { "id": "05_nuclear", - "group": "Other 1", + "group": "other 1", "name": "05_nuclear", "enabled": true, "unitcount": 1, @@ -846,7 +846,7 @@ }, { "id": "06_coal", - "group": "Other 1", + "group": "other 1", "name": "06_coal", "enabled": true, "unitcount": 1, @@ -870,7 +870,7 @@ }, { "id": "07_gas", - "group": "Other 1", + "group": "other 1", "name": "07_gas", "enabled": true, "unitcount": 1, @@ -894,7 +894,7 @@ }, { "id": "08_non-res", - "group": "Other 1", + "group": "other 1", "name": "08_non-res", "enabled": true, "unitcount": 1, @@ -918,7 +918,7 @@ }, { "id": "09_hydro_pump", - "group": "Other 1", + "group": "other 1", "name": "09_hydro_pump", "enabled": true, "unitcount": 1, diff --git a/tests/integration/study_data_blueprint/test_binding_constraints.py b/tests/integration/study_data_blueprint/test_binding_constraints.py index 192f1d27b2..beb0036ba4 100644 --- a/tests/integration/study_data_blueprint/test_binding_constraints.py +++ b/tests/integration/study_data_blueprint/test_binding_constraints.py @@ -193,7 +193,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st assert len(clusters_list) == 1 assert clusters_list[0]["id"] == cluster_id assert clusters_list[0]["name"] == "Cluster 1" - assert clusters_list[0]["group"] == "Nuclear" + assert clusters_list[0]["group"] == "nuclear" if study_type == "variant": study_id = preparer.create_variant(study_id, name="Variant 1") @@ -1262,15 +1262,15 @@ def test_for_version_870(self, client: TestClient, user_access_token: str, study res = client.get(f"/v1/studies/{study_id}/constraint-groups") assert res.status_code in {200, 201}, res.json() groups = res.json() - assert set(groups) == {"default", "random_grp", "Group 1", "Group 2"} - assert groups["Group 2"] == [ + assert set(groups) == {"default", "random_grp", "group 1", "group 2"} + assert groups["group 2"] == [ { "comments": "New API", "terms": [], "enabled": True, "filterSynthesis": "", "filterYearByYear": "", - "group": "Group 2", + "group": "group 2", "id": "second bc", "name": "Second BC", "operator": "less", diff --git a/tests/integration/study_data_blueprint/test_renewable.py b/tests/integration/study_data_blueprint/test_renewable.py index 37a06728ae..d69ff1e4ad 100644 --- a/tests/integration/study_data_blueprint/test_renewable.py +++ b/tests/integration/study_data_blueprint/test_renewable.py @@ -45,7 +45,7 @@ from antarest.core.tasks.model import TaskStatus from antarest.core.utils.string import to_camel_case -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableProperties from tests.integration.utils import wait_task_completion @@ -131,8 +131,9 @@ def test_lifecycle( fr_solar_pv_id = res.json()["id"] assert fr_solar_pv_id == transform_name_to_id(fr_solar_pv, lower=False) # noinspection SpellCheckingInspection - fr_solar_pv_cfg = {"id": fr_solar_pv_id, **fr_solar_pv_props} - assert res.json() == fr_solar_pv_cfg + expected_fr_solar_pv_cfg = {"id": fr_solar_pv_id, **fr_solar_pv_props} + expected_fr_solar_pv_cfg["group"] = "solar pv" + assert res.json() == expected_fr_solar_pv_cfg # reading the properties of a renewable cluster res = client.get( @@ -140,7 +141,7 @@ def test_lifecycle( headers={"Authorization": f"Bearer {user_access_token}"}, ) assert res.status_code == 200, res.json() - assert res.json() == fr_solar_pv_cfg + assert res.json() == expected_fr_solar_pv_cfg # ============================= # RENEWABLE CLUSTER MATRICES @@ -174,7 +175,7 @@ def test_lifecycle( headers={"Authorization": f"Bearer {user_access_token}"}, ) assert res.status_code == 200, res.json() - assert res.json() == EXISTING_CLUSTERS + [fr_solar_pv_cfg] + assert res.json() == EXISTING_CLUSTERS + [expected_fr_solar_pv_cfg] # updating properties res = client.patch( @@ -186,19 +187,19 @@ def test_lifecycle( }, ) assert res.status_code == 200, res.json() - fr_solar_pv_cfg = { - **fr_solar_pv_cfg, + expected_fr_solar_pv_cfg = { + **expected_fr_solar_pv_cfg, "name": "FR Solar pv old 1", "nominalCapacity": 5132, } - assert res.json() == fr_solar_pv_cfg + assert res.json() == expected_fr_solar_pv_cfg res = client.get( f"/v1/studies/{internal_study_id}/areas/{area_id}/clusters/renewable/{fr_solar_pv_id}", headers={"Authorization": f"Bearer {user_access_token}"}, ) assert res.status_code == 200, res.json() - assert res.json() == fr_solar_pv_cfg + assert res.json() == expected_fr_solar_pv_cfg # =========================== # RENEWABLE CLUSTER UPDATE @@ -213,13 +214,13 @@ def test_lifecycle( "tsInterpretation": "power-generation", }, ) - fr_solar_pv_cfg = { - **fr_solar_pv_cfg, + expected_fr_solar_pv_cfg = { + **expected_fr_solar_pv_cfg, "nominalCapacity": 2260, "tsInterpretation": "power-generation", } assert res.status_code == 200, res.json() - assert res.json() == fr_solar_pv_cfg + assert res.json() == expected_fr_solar_pv_cfg # An attempt to update the `unitCount` property with an invalid value # should raise a validation error. @@ -239,7 +240,7 @@ def test_lifecycle( headers={"Authorization": f"Bearer {user_access_token}"}, ) assert res.status_code == 200, res.json() - assert res.json() == fr_solar_pv_cfg + assert res.json() == expected_fr_solar_pv_cfg # =============================== # RENEWABLE CLUSTER DUPLICATION @@ -253,7 +254,7 @@ def test_lifecycle( ) # asserts the config is the same assert res.status_code in {200, 201}, res.json() - duplicated_config = dict(fr_solar_pv_cfg) + duplicated_config = dict(expected_fr_solar_pv_cfg) duplicated_config["name"] = new_name duplicated_id = transform_name_to_id(new_name, lower=False) duplicated_config["id"] = duplicated_id @@ -425,7 +426,7 @@ def test_lifecycle( assert res.status_code == 200, res.json() obj = res.json() # If a group is not found, return the default group ("Other RES 1" by default). - assert obj["group"] == "Other RES 1" + assert obj["group"] == "other res 1" # Check PATCH with the wrong `area_id` res = client.patch( @@ -587,7 +588,7 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var res = client.get(f"/v1/studies/{variant_id}/areas/{area_id}/clusters/renewable/{new_id}") assert res.status_code == 200, res.json() cluster_cfg = res.json() - assert cluster_cfg["group"] == "Wind Offshore" + assert cluster_cfg["group"] == "wind offshore" assert cluster_cfg["unitCount"] == 15 assert cluster_cfg["nominalCapacity"] == 42500 diff --git a/tests/integration/study_data_blueprint/test_st_storage.py b/tests/integration/study_data_blueprint/test_st_storage.py index 1a654ed9f9..32b90b926b 100644 --- a/tests/integration/study_data_blueprint/test_st_storage.py +++ b/tests/integration/study_data_blueprint/test_st_storage.py @@ -21,7 +21,7 @@ from antarest.core.tasks.model import TaskStatus from antarest.study.business.areas.st_storage_management import create_storage_output -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import create_st_storage_config from tests.integration.utils import wait_task_completion @@ -149,7 +149,7 @@ def test_lifecycle__nominal( # Unfilled properties will be set to their default values. siemens_properties = { "name": siemens_battery, - "group": "Battery", + "group": "battery", "injectionNominalCapacity": 1450, "withdrawalNominalCapacity": 1350, "reservoirCapacity": 1500, @@ -341,7 +341,7 @@ def test_lifecycle__nominal( # In the following example, we will create two short-term storages: siemens_properties = { "name": siemens_battery, - "group": "Battery", + "group": "battery", "injectionNominalCapacity": 1450, "withdrawalNominalCapacity": 1350, "reservoirCapacity": 1500, @@ -361,7 +361,7 @@ def test_lifecycle__nominal( grand_maison = "Grand'Maison" grand_maison_properties = { "name": grand_maison, - "group": "PSP_closed", + "group": "PSP_Closed", "injectionNominalCapacity": 1500, "withdrawalNominalCapacity": 1800, "reservoirCapacity": 20000, @@ -385,6 +385,7 @@ def test_lifecycle__nominal( assert res.status_code == 200, res.json() siemens_output = {**default_output, **siemens_properties, "id": siemens_battery_id} grand_maison_output = {**default_output, **grand_maison_properties, "id": grand_maison_id} + grand_maison_output["group"] = "psp_closed" assert res.json() == [duplicated_output, siemens_output, grand_maison_output] # We can delete the three short-term storages at once. @@ -612,7 +613,7 @@ def test__default_values( ) assert res.status_code == 200, res.json() tesla_battery_id = res.json()["id"] - tesla_output = {**default_output, "id": tesla_battery_id, "name": tesla_battery, "group": "Battery"} + tesla_output = {**default_output, "id": tesla_battery_id, "name": tesla_battery, "group": "battery"} assert res.json() == tesla_output # Use the Debug mode to make sure that the initialLevel and initialLevelOptim properties @@ -623,7 +624,7 @@ def test__default_values( ) assert res.status_code == 200, res.json() actual = res.json() - expected = {**default_config, "name": tesla_battery, "group": "Battery"} + expected = {**default_config, "name": tesla_battery, "group": "battery"} assert actual == expected # We want to make sure that the default properties are applied to a study variant. @@ -653,14 +654,14 @@ def test__default_values( "action": "create_st_storage", "args": { "area_id": "fr", - "parameters": {**default_config, "name": siemens_battery, "group": "Battery"}, + "parameters": {**default_config, "name": siemens_battery, "group": "battery"}, "pmax_injection": ANY, "pmax_withdrawal": ANY, "lower_rule_curve": ANY, "upper_rule_curve": ANY, "inflows": ANY, }, - "version": 1, + "version": 2, "updated_at": ANY, "user_name": ANY, } @@ -735,7 +736,7 @@ def test__default_values( expected = { **default_config, "name": siemens_battery, - "group": "Battery", + "group": "battery", "injectionnominalcapacity": 1600, "initiallevel": 0.0, } @@ -827,7 +828,7 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var res = client.get(f"/v1/studies/{variant_id}/areas/{area_id}/storages/{new_id}") assert res.status_code == 200, res.json() cluster_cfg = res.json() - assert cluster_cfg["group"] == "Battery" + assert cluster_cfg["group"] == "battery" assert cluster_cfg["injectionNominalCapacity"] == 4500 assert cluster_cfg["withdrawalNominalCapacity"] == 4230 assert cluster_cfg["reservoirCapacity"] == 5600 diff --git a/tests/integration/study_data_blueprint/test_table_mode.py b/tests/integration/study_data_blueprint/test_table_mode.py index f2e53ba347..74e9cbde86 100644 --- a/tests/integration/study_data_blueprint/test_table_mode.py +++ b/tests/integration/study_data_blueprint/test_table_mode.py @@ -417,7 +417,7 @@ def test_lifecycle__nominal( "enabled": True, "fixedCost": 0, "genTs": "use global", - "group": "Other 2", + "group": "other 2", "lawForced": "uniform", "lawPlanned": "uniform", "marginalCost": 10, @@ -444,7 +444,7 @@ def test_lifecycle__nominal( "enabled": True, "fixedCost": 0, "genTs": "use global", - "group": "Nuclear", + "group": "nuclear", "lawForced": "uniform", "lawPlanned": "uniform", "marginalCost": 20, @@ -489,42 +489,42 @@ def test_lifecycle__nominal( assert res.status_code == 200, res.json() expected: t.Dict[str, t.Dict[str, t.Any]] expected = { - "de / 01_solar": {"group": "Other 2", "nominalCapacity": 500000, "unitCount": 17}, - "de / 02_wind_on": {"group": "Nuclear", "nominalCapacity": 314159, "unitCount": 15}, - "de / 03_wind_off": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "de / 04_res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "de / 05_nuclear": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "de / 06_coal": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "de / 07_gas": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "de / 08_non-res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "de / 09_hydro_pump": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 01_solar": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 02_wind_on": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 03_wind_off": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 04_res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 05_nuclear": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 06_coal": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 07_gas": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 08_non-res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "es / 09_hydro_pump": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 01_solar": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 02_wind_on": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 03_wind_off": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 04_res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 05_nuclear": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 06_coal": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 07_gas": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 08_non-res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "fr / 09_hydro_pump": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 01_solar": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 02_wind_on": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 03_wind_off": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 04_res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 05_nuclear": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 06_coal": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 07_gas": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 08_non-res": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, - "it / 09_hydro_pump": {"group": "Other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 01_solar": {"group": "other 2", "nominalCapacity": 500000, "unitCount": 17}, + "de / 02_wind_on": {"group": "nuclear", "nominalCapacity": 314159, "unitCount": 15}, + "de / 03_wind_off": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 04_res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 05_nuclear": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 06_coal": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 07_gas": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 08_non-res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "de / 09_hydro_pump": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 01_solar": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 02_wind_on": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 03_wind_off": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 04_res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 05_nuclear": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 06_coal": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 07_gas": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 08_non-res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "es / 09_hydro_pump": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 01_solar": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 02_wind_on": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 03_wind_off": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 04_res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 05_nuclear": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 06_coal": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 07_gas": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 08_non-res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "fr / 09_hydro_pump": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 01_solar": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 02_wind_on": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 03_wind_off": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 04_res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 05_nuclear": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 06_coal": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 07_gas": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 08_non-res": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, + "it / 09_hydro_pump": {"group": "other 1", "nominalCapacity": 1000000, "unitCount": 1}, } if study_version >= 860: for key in expected: @@ -634,7 +634,7 @@ def test_lifecycle__nominal( json={ "fr / Dieppe": {"enabled": False}, "fr / La Rochelle": {"enabled": True, "nominalCapacity": 3.1, "unitCount": 2}, - "it / Pouilles": {"group": "Wind Onshore"}, + "it / Pouilles": {"group": "wind onshore"}, }, ) assert res.status_code == 200, res.json() @@ -645,12 +645,12 @@ def test_lifecycle__nominal( ) assert res.status_code == 200, res.json() expected = { - "fr / Dieppe": {"enabled": False, "group": "Wind Offshore", "nominalCapacity": 8, "unitCount": 62}, - "fr / La Rochelle": {"enabled": True, "group": "Solar PV", "nominalCapacity": 3.1, "unitCount": 2}, - "fr / Oleron": {"enabled": True, "group": "Wind Offshore", "nominalCapacity": 15, "unitCount": 70}, - "it / Pouilles": {"enabled": False, "group": "Wind Onshore", "nominalCapacity": 11, "unitCount": 40}, - "it / Sardaigne": {"enabled": True, "group": "Wind Offshore", "nominalCapacity": 12, "unitCount": 86}, - "it / Sicile": {"enabled": True, "group": "Solar PV", "nominalCapacity": 1.8, "unitCount": 1}, + "fr / Dieppe": {"enabled": False, "group": "wind offshore", "nominalCapacity": 8, "unitCount": 62}, + "fr / La Rochelle": {"enabled": True, "group": "solar pv", "nominalCapacity": 3.1, "unitCount": 2}, + "fr / Oleron": {"enabled": True, "group": "wind offshore", "nominalCapacity": 15, "unitCount": 70}, + "it / Pouilles": {"enabled": False, "group": "wind onshore", "nominalCapacity": 11, "unitCount": 40}, + "it / Sardaigne": {"enabled": True, "group": "wind offshore", "nominalCapacity": 12, "unitCount": 86}, + "it / Sicile": {"enabled": True, "group": "solar pv", "nominalCapacity": 1.8, "unitCount": 1}, } actual = res.json() assert actual == expected @@ -753,7 +753,7 @@ def test_lifecycle__nominal( # "name": "Siemens", "efficiency": 1, "enabled": None, - "group": "Battery", + "group": "battery", "initialLevel": 0.5, "initialLevelOptim": False, "injectionNominalCapacity": 1550, @@ -765,7 +765,7 @@ def test_lifecycle__nominal( # "name": "Tesla", "efficiency": 0.75, "enabled": None, - "group": "Battery", + "group": "battery", "initialLevel": 0.89, "initialLevelOptim": False, "injectionNominalCapacity": 1200, @@ -777,7 +777,7 @@ def test_lifecycle__nominal( # "name": "storage3", "efficiency": 1, "enabled": None, - "group": "Pondage", + "group": "pondage", "initialLevel": 1, "initialLevelOptim": False, "injectionNominalCapacity": 1234, @@ -789,7 +789,7 @@ def test_lifecycle__nominal( # "name": "storage4", "efficiency": 1, "enabled": None, - "group": "PSP_open", + "group": "psp_open", "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 567, @@ -822,25 +822,25 @@ def test_lifecycle__nominal( assert res.status_code == 200, res.json() expected = { "fr / siemens": { - "group": "Battery", + "group": "battery", "injectionNominalCapacity": 1550, "reservoirCapacity": 1500, "withdrawalNominalCapacity": 1550, }, "fr / tesla": { - "group": "Battery", + "group": "battery", "injectionNominalCapacity": 1200, "reservoirCapacity": 1200, "withdrawalNominalCapacity": 1200, }, "it / storage3": { - "group": "Pondage", + "group": "pondage", "injectionNominalCapacity": 1234, "reservoirCapacity": 1357, "withdrawalNominalCapacity": 1020, }, "it / storage4": { - "group": "PSP_open", + "group": "psp_open", "injectionNominalCapacity": 567, "reservoirCapacity": 500, "withdrawalNominalCapacity": 456, @@ -859,7 +859,7 @@ def test_lifecycle__nominal( f"/v1/studies/{internal_study_id}/areas/{fr_id}/clusters/thermal", json={ "name": "Cluster 1", - "group": "Nuclear", + "group": "nuclear", }, ) assert res.status_code == 200, res.json() @@ -950,7 +950,7 @@ def test_lifecycle__nominal( if study_version >= 870: expected_binding["binding constraint 1"]["group"] = "default" - expected_binding["binding constraint 2"]["group"] = "My BC Group" + expected_binding["binding constraint 2"]["group"] = "my bc group" assert actual == expected_binding diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index 0ee1737d66..1c106a8c87 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -49,7 +49,7 @@ from starlette.testclient import TestClient from antarest.core.utils.string import to_camel_case -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalProperties from tests.integration.utils import wait_task_completion @@ -63,7 +63,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "01_solar", "lawForced": "uniform", "lawPlanned": "uniform", @@ -87,7 +87,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "02_wind_on", "lawForced": "uniform", "lawPlanned": "uniform", @@ -111,7 +111,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "03_wind_off", "lawForced": "uniform", "lawPlanned": "uniform", @@ -135,7 +135,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "04_res", "lawForced": "uniform", "lawPlanned": "uniform", @@ -159,7 +159,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "05_nuclear", "lawForced": "uniform", "lawPlanned": "uniform", @@ -183,7 +183,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "06_coal", "lawForced": "uniform", "lawPlanned": "uniform", @@ -207,7 +207,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "07_gas", "lawForced": "uniform", "lawPlanned": "uniform", @@ -231,7 +231,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "08_non-res", "lawForced": "uniform", "lawPlanned": "uniform", @@ -255,7 +255,7 @@ "enabled": True, "fixedCost": 0.0, "genTs": "use global", - "group": "Other 1", + "group": "other 1", "id": "09_hydro_pump", "lawForced": "uniform", "lawPlanned": "uniform", @@ -358,7 +358,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st fr_gas_conventional_props = { **DEFAULT_PROPERTIES, "name": fr_gas_conventional, - "group": "Gas", + "group": "gas", "unitCount": 15, "nominalCapacity": 31.6, "minStablePower": 5.4984, @@ -756,7 +756,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.status_code == 200, res.json() obj = res.json() # If a group is not found, return the default group ('OTHER1' by default). - assert obj["group"] == "Other 1" + assert obj["group"] == "other 1" # Check PATCH with the wrong `area_id` res = client.patch( @@ -925,7 +925,7 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var res = client.get(f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal/{new_id}") assert res.status_code == 200, res.json() cluster_cfg = res.json() - assert cluster_cfg["group"] == "Nuclear" + assert cluster_cfg["group"] == "nuclear" assert cluster_cfg["unitCount"] == 13 assert cluster_cfg["nominalCapacity"] == 42500 assert cluster_cfg["marginalCost"] == 0.2 diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 003aca1828..df4d67e33a 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -533,19 +533,19 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: { "code-oi": None, "enabled": True, - "group": None, + "group": "other 1", "id": "cluster 1", - "marginal-cost": None, - "market-bid-cost": None, - "min-down-time": None, - "min-stable-power": None, - "min-up-time": None, + "marginal-cost": 0.0, + "market-bid-cost": 0.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, "name": "cluster 1", - "nominalcapacity": 0, - "spinning": None, - "spread-cost": None, + "nominalcapacity": 0.0, + "spinning": 0.0, + "spread-cost": 0.0, "type": None, - "unitcount": 0, + "unitcount": 1, } ], "type": "AREA", @@ -559,19 +559,19 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: { "code-oi": None, "enabled": True, - "group": None, + "group": "other 1", "id": "cluster 2", - "marginal-cost": None, - "market-bid-cost": None, - "min-down-time": None, - "min-stable-power": None, - "min-up-time": None, + "marginal-cost": 0.0, + "market-bid-cost": 0.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, "name": "cluster 2", "nominalcapacity": 2.5, - "spinning": None, - "spread-cost": None, + "spinning": 0.0, + "spread-cost": 0.0, "type": None, - "unitcount": 0, + "unitcount": 1, } ], "type": "AREA", @@ -580,7 +580,7 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: "id": "all areas", "metadata": {"country": None, "tags": []}, "name": "All areas", - "set": ANY, # because some time the order is not the same + "set": ["area 1", "area 2"], "thermals": None, "type": "DISTRICT", }, @@ -1250,7 +1250,7 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: ) expected = { "enabled": False, - "group": "Other RES 1", # Default group used when not specified. + "group": "other res 1", "id": "cluster renewable 1", "name": "cluster renewable 1 renamed", "nominalCapacity": 3.0, @@ -1263,7 +1263,7 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: # Thermal form obj = { - "group": "Lignite", + "group": "lignite", "name": "cluster 1 renamed", "unitCount": 3, "enabled": False, @@ -1384,19 +1384,19 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: { "code-oi": None, "enabled": True, - "group": None, + "group": "other 1", "id": "cluster 2", - "marginal-cost": None, - "market-bid-cost": None, - "min-down-time": None, - "min-stable-power": None, - "min-up-time": None, + "marginal-cost": 0.0, + "market-bid-cost": 0.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, "name": "cluster 2", "nominalcapacity": 2.5, - "spinning": None, - "spread-cost": None, + "spinning": 0.0, + "spread-cost": 0.0, "type": None, - "unitcount": 0, + "unitcount": 1, } ], "type": "AREA", diff --git a/tests/integration/test_integration_token_end_to_end.py b/tests/integration/test_integration_token_end_to_end.py index c8c987ce34..2ebf2e95ff 100644 --- a/tests/integration/test_integration_token_end_to_end.py +++ b/tests/integration/test_integration_token_end_to_end.py @@ -96,7 +96,7 @@ def test_nominal_case_of_an_api_user(client: TestClient, admin_access_token: str "cluster_name": "mycluster", "parameters": { "group": "Gas", - "unitCount": 1, + "unitcount": 1, "marginal_cost": 50, }, }, @@ -116,15 +116,15 @@ def test_nominal_case_of_an_api_user(client: TestClient, admin_access_token: str "parameters": { "group": "Gas", "marginal-cost": 98, - "unitCount": 1, - "nominalCapacity": 250, - "minStablePower": 0.0, - "minUpTime": 2, - "minDownTime": 2, + "unitcount": 1, + "nominalcapacity": 250, + "min-stable-power": 0.0, + "min-up-time": 2, + "min-down-time": 2, "spinning": 5, - "spreadCost": 0.0, - "startupCost": 2500, - "marketBidCost": 85, + "spread-cost": 0.0, + "startup-cost": 2500, + "market-bid-cost": 85, "co2": 0.3, }, }, diff --git a/tests/integration/variant_blueprint/test_renewable_cluster.py b/tests/integration/variant_blueprint/test_renewable_cluster.py index 098cb12d52..848cbe1336 100644 --- a/tests/integration/variant_blueprint/test_renewable_cluster.py +++ b/tests/integration/variant_blueprint/test_renewable_cluster.py @@ -17,7 +17,7 @@ from starlette.testclient import TestClient from antarest.core.tasks.model import TaskStatus -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from tests.integration.utils import wait_task_completion @@ -124,7 +124,7 @@ def test_lifecycle( properties = res.json() expected = { "enabled": True, - "group": "Wind Offshore", + "group": "wind offshore", "id": "Oleron", "name": cluster_fr1, "nominalCapacity": 2500.0, @@ -141,7 +141,7 @@ def test_lifecycle( properties = res.json() expected = { "enabled": False, - "group": "Solar PV", + "group": "solar pv", "id": "La_Rochelle", "name": cluster_fr2, "nominalCapacity": 3500.0, @@ -204,7 +204,6 @@ def test_lifecycle( cluster_it1_id = transform_name_to_id(cluster_it1, lower=False) args = { "area_id": area_it_id, - "cluster_name": cluster_it1_id, "parameters": { "group": "wind offshore", "name": cluster_it1, @@ -216,7 +215,7 @@ def test_lifecycle( res = client.post( f"/v1/studies/{internal_study_id}/commands", headers={"Authorization": f"Bearer {user_access_token}"}, - json=[{"action": "create_renewables_cluster", "args": args}], + json=[{"action": "create_renewables_cluster", "version": 2, "args": args}], ) res.raise_for_status() @@ -228,7 +227,7 @@ def test_lifecycle( properties = res.json() expected = { "enabled": True, - "group": "Wind Offshore", + "group": "wind offshore", "id": "Ol ron", "name": cluster_it1, "nominalCapacity": 1000.0, @@ -272,20 +271,23 @@ def test_lifecycle( "es": {"list": {}}, "fr": { "list": { - cluster_fr1_id: { + "Oleron": { + "enabled": True, "group": "wind offshore", - "name": cluster_fr1, - "nominalcapacity": 2500, + "name": "Oleron", + "nominalcapacity": 2500.0, "ts-interpretation": "power-generation", - }, + "unitcount": 1, + } } }, "it": { "list": { - cluster_it1_id: { + "Ol ron": { + "enabled": True, "group": "wind offshore", - "name": cluster_it1, - "nominalcapacity": 1000, + "name": "Oléron", + "nominalcapacity": 1000.0, "ts-interpretation": "production-factor", "unitcount": 1, } @@ -315,10 +317,11 @@ def test_lifecycle( "fr": {"list": {}}, "it": { "list": { - cluster_it1_id: { + "Ol ron": { + "enabled": True, "group": "wind offshore", - "name": cluster_it1, - "nominalcapacity": 1000, + "name": "Oléron", + "nominalcapacity": 1000.0, "ts-interpretation": "production-factor", "unitcount": 1, } diff --git a/tests/integration/variant_blueprint/test_st_storage.py b/tests/integration/variant_blueprint/test_st_storage.py index 52ff483836..4eafdd8789 100644 --- a/tests/integration/variant_blueprint/test_st_storage.py +++ b/tests/integration/variant_blueprint/test_st_storage.py @@ -18,7 +18,7 @@ from starlette.testclient import TestClient from antarest.core.tasks.model import TaskStatus -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from tests.integration.utils import wait_task_completion @@ -118,7 +118,7 @@ def test_lifecycle( res = client.post( f"/v1/studies/{internal_study_id}/commands", headers={"Authorization": f"Bearer {user_access_token}"}, - json=[{"action": "create_st_storage", "args": args}], + json=[{"action": "create_st_storage", "version": 2, "args": args}], ) res.raise_for_status() @@ -180,7 +180,7 @@ def test_lifecycle( res = client.post( f"/v1/studies/{internal_study_id}/commands", headers={"Authorization": f"Bearer {user_access_token}"}, - json=[{"action": "create_st_storage", "args": args}], + json=[{"action": "create_st_storage", "version": 2, "args": args}], ) res.raise_for_status() diff --git a/tests/integration/variant_blueprint/test_thermal_cluster.py b/tests/integration/variant_blueprint/test_thermal_cluster.py index 5029c2bca3..e00d5b43b5 100644 --- a/tests/integration/variant_blueprint/test_thermal_cluster.py +++ b/tests/integration/variant_blueprint/test_thermal_cluster.py @@ -20,7 +20,7 @@ from starlette.testclient import TestClient from antarest.core.tasks.model import TaskDTO, TaskStatus -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id def _create_thermal_params(cluster_name: str) -> t.Mapping[str, t.Any]: diff --git a/tests/storage/business/test_study_version_upgrader.py b/tests/storage/business/test_study_version_upgrader.py index 4f0e6d6e51..2660397c12 100644 --- a/tests/storage/business/test_study_version_upgrader.py +++ b/tests/storage/business/test_study_version_upgrader.py @@ -25,7 +25,7 @@ from antarest.core.exceptions import UnsupportedStudyVersion from antarest.core.serde.ini_reader import IniReader -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.root.settings.generaldata import DUPLICATE_KEYS from antarest.study.storage.study_upgrader import ( InvalidUpgrade, diff --git a/tests/storage/repository/filesystem/config/test_utils.py b/tests/storage/repository/filesystem/config/test_utils.py index fcce124a3c..cd44b5af85 100644 --- a/tests/storage/repository/filesystem/config/test_utils.py +++ b/tests/storage/repository/filesystem/config/test_utils.py @@ -14,7 +14,7 @@ import pytest -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id VALID_CHARS = "azAZ09_-(),&" diff --git a/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py b/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py new file mode 100644 index 0000000000..a4d70ba111 --- /dev/null +++ b/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py @@ -0,0 +1,165 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import textwrap +from pathlib import Path +from unittest.mock import Mock + +import pytest +from antares.study.version import StudyVersion + +from antarest.core.serde.ini_reader import IniReader +from antarest.study.model import STUDY_VERSION_8_6, STUDY_VERSION_8_8 +from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.root.input.bindingconstraints.bindingconstraints_ini import ( + BindingConstraintsIni, +) +from antarest.study.storage.rawstudy.model.filesystem.root.input.renewables.clusters import ( + ClusteredRenewableClusterConfig, +) +from antarest.study.storage.rawstudy.model.filesystem.root.input.st_storage.clusters.area.list import ( + InputSTStorageAreaList, +) +from antarest.study.storage.rawstudy.model.filesystem.root.input.thermal.cluster.area.list import ( + InputThermalClustersAreaList, +) + + +@pytest.fixture +def study_dir(tmp_path: Path) -> Path: + study_path = tmp_path / "study" + study_path.mkdir() + return study_path + + +@pytest.fixture +def ini_file(study_dir: Path) -> Path: + file_path = study_dir / "test.ini" + file_path.touch() + return file_path + + +def create_study_config(study_dir: Path, ini_file: Path, version: StudyVersion, area_name: str) -> FileStudyTreeConfig: + area = Area( + name=area_name, links={}, thermals=[], renewables=[], filters_synthesis=[], filters_year=[], st_storages=[] + ) + areas = {area_name: area} + return FileStudyTreeConfig(study_path=study_dir, path=ini_file, version=version, study_id="id", areas=areas) + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "ini_node_cluster_class", + [InputSTStorageAreaList, ClusteredRenewableClusterConfig, InputThermalClustersAreaList], +) +def test_group_is_parsed_to_lower_case(study_dir: Path, ini_file: Path, ini_node_cluster_class): + ini_file.write_text( + textwrap.dedent( + """ + [Cluster 1] + group = Gas + """ + ) + ) + + node = ini_node_cluster_class( + context=Mock(), + config=create_study_config(study_dir, ini_file, STUDY_VERSION_8_8, "area_test"), + area="area_test", + ) + + assert node.get() == {"Cluster 1": {"group": "gas"}} + + +@pytest.mark.unit_test +@pytest.mark.parametrize( + "ini_node_cluster_class", + [InputSTStorageAreaList, ClusteredRenewableClusterConfig, InputThermalClustersAreaList], +) +def test_cluster_ini_list(study_dir: Path, ini_file: Path, ini_node_cluster_class): + data = {"Cluster 1": {"group": "Gas"}} + node = ini_node_cluster_class( + context=Mock(), + config=create_study_config(study_dir, ini_file, STUDY_VERSION_8_8, "area_test"), + area="area_test", + ) + + node.save(data) + # Asserts the data is saved correctly + ini_content = IniReader().read(ini_file) + assert ini_content == {"Cluster 1": {"group": "gas"}} + # Asserts cluster group is returned in lower case + content = node.get([]) + assert content == {"Cluster 1": {"group": "gas"}} + # Asserts saving the group in upper case works and that it will be returned in lower case + node.save("NUCLEAR", ["Cluster 1", "group"]) + content = node.get([]) + assert content == {"Cluster 1": {"group": "nuclear"}} + + +@pytest.mark.unit_test +def test_binding_constraint_group_writing( + study_dir: Path, + ini_file: Path, +): + node = BindingConstraintsIni( + context=Mock(), + config=FileStudyTreeConfig(study_path=study_dir, path=ini_file, version=STUDY_VERSION_8_8, study_id="id"), + ) + + node.save({"0": {"name": "BC_1", "group": "GRP_1"}}) + assert IniReader().read(ini_file) == {"0": {"name": "BC_1", "group": "grp_1"}} + + node.save(data="GRP_2", url=["0", "group"]) + assert IniReader().read(ini_file) == {"0": {"name": "BC_1", "group": "grp_2"}} + + +@pytest.mark.unit_test +def test_binding_constraint_group_parsing( + study_dir: Path, + ini_file: Path, +): + ini_file.write_text( + textwrap.dedent( + """ + [0] + group = GRP + """ + ) + ) + + node = BindingConstraintsIni( + context=Mock(), + config=FileStudyTreeConfig(study_path=study_dir, path=ini_file, version=STUDY_VERSION_8_8, study_id="id"), + ) + + content = node.get() + assert content == {"0": {"group": "grp"}} + + +@pytest.mark.unit_test +def test_st_storage_group_is_written_to_title_case_for_8_6(study_dir: Path, ini_file: Path): + ini_file.write_text( + textwrap.dedent( + """ + [Cluster 1] + group = Gas + """ + ) + ) + node = InputSTStorageAreaList( + context=Mock(), + config=create_study_config(study_dir, ini_file, STUDY_VERSION_8_6, "area_test"), + area="area_test", + ) + + node.save({"Cluster 1": {"group": "GAS"}}) + assert IniReader().read(ini_file) == {"Cluster 1": {"group": "Gas"}} diff --git a/tests/study/storage/rawstudy/test_raw_study_service.py b/tests/study/storage/rawstudy/test_raw_study_service.py index c2f2b8a9c0..4154362c41 100644 --- a/tests/study/storage/rawstudy/test_raw_study_service.py +++ b/tests/study/storage/rawstudy/test_raw_study_service.py @@ -25,7 +25,11 @@ from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import RawStudy, StudyAdditionalData from antarest.study.storage.patch_service import PatchService -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig, STStorageGroup +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( + STStorageConfig, + STStorageGroup, + STStorageProperties, +) from antarest.study.storage.rawstudy.raw_study_service import RawStudyService from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants @@ -131,8 +135,7 @@ def test_export_study_flat( create_st_storage = CreateSTStorage( command_context=command_context, area_id="fr", - parameters=STStorageConfig( - id="", # will be calculated ;-) + parameters=STStorageProperties( name="Storage1", group=STStorageGroup.BATTERY, injection_nominal_capacity=1500, diff --git a/tests/study/storage/variantstudy/test_snapshot_generator.py b/tests/study/storage/variantstudy/test_snapshot_generator.py index 6956502504..5a7a3c96c1 100644 --- a/tests/study/storage/variantstudy/test_snapshot_generator.py +++ b/tests/study/storage/variantstudy/test_snapshot_generator.py @@ -10,7 +10,6 @@ # # This file is part of the Antares project. -import configparser import datetime import json import logging @@ -29,6 +28,7 @@ from antarest.core.jwt import JWTGroup, JWTUser from antarest.core.requests import RequestParameters from antarest.core.roles import RoleType +from antarest.core.serde.ini_reader import IniReader from antarest.core.tasks.service import ITaskNotifier from antarest.core.utils.fastapi_sqlalchemy import db from antarest.login.model import Group, Role, User @@ -920,18 +920,46 @@ def test_generate__nominal_case( assert snapshot_dir.exists() assert (snapshot_dir / "study.antares").exists() assert (snapshot_dir / "input/areas/list.txt").read_text().splitlines(keepends=False) == ["North", "South"] - config = configparser.RawConfigParser() - config.read(snapshot_dir / "input/links/north/properties.ini") - assert config.sections() == ["south"] - assert config["south"], "The 'south' section must exist in the 'properties.ini' file." - config = configparser.RawConfigParser() - config.read(snapshot_dir / "input/thermal/clusters/south/list.ini") - assert config.sections() == ["gas_cluster"] - assert config["gas_cluster"] == { # type: ignore - "group": "Gas", - "unitcount": "1", - "nominalcapacity": "500", + reader = IniReader() + properties = reader.read(snapshot_dir / "input/links/north/properties.ini") + assert list(properties.keys()) == ["south"] + reader = IniReader() + cluster_props = reader.read(snapshot_dir / "input/thermal/clusters/south/list.ini") + assert list(cluster_props.keys()) == ["gas_cluster"] + assert cluster_props["gas_cluster"] == { + "co2": 0.0, + "enabled": True, + "fixed-cost": 0.0, + "gen-ts": "use global", + "group": "gas", + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 0.0, + "market-bid-cost": 0.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, + "must-run": False, "name": "gas_cluster", + "nh3": 0.0, + "nmvoc": 0.0, + "nominalcapacity": 500.0, + "nox": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0, + "pm10": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "so2": 0.0, + "spinning": 0.0, + "spread-cost": 0.0, + "startup-cost": 0.0, + "unitcount": 1, + "volatility.forced": 0.0, + "volatility.planned": 0.0, } # Check: the matrices are not denormalized (we should have links to matrices). diff --git a/tests/study/storage/variantstudy/test_variant_study_service.py b/tests/study/storage/variantstudy/test_variant_study_service.py index a878a4cdf4..a58399e097 100644 --- a/tests/study/storage/variantstudy/test_variant_study_service.py +++ b/tests/study/storage/variantstudy/test_variant_study_service.py @@ -30,7 +30,11 @@ from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import RawStudy, StudyAdditionalData from antarest.study.storage.patch_service import PatchService -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig, STStorageGroup +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( + STStorageConfig, + STStorageGroup, + STStorageProperties, +) from antarest.study.storage.rawstudy.raw_study_service import RawStudyService from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants @@ -200,8 +204,7 @@ def test_generate_task( create_st_storage = CreateSTStorage( command_context=command_context, area_id="fr", - parameters=STStorageConfig( - id="", # will be calculated ;-) + parameters=STStorageProperties( name="Storage1", group=STStorageGroup.BATTERY, injection_nominal_capacity=1500, diff --git a/tests/study/test_field_validators.py b/tests/study/test_field_validators.py new file mode 100644 index 0000000000..0f1d5414b4 --- /dev/null +++ b/tests/study/test_field_validators.py @@ -0,0 +1,47 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +from typing import Annotated + +import pytest +from mypy.exprtotype import ANNOTATED_TYPE_NAMES +from pydantic import BaseModel, Field, TypeAdapter, ValidationError + +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import AreaId, ItemName + + +def test_item_name_object_is_invalid(): + with pytest.raises(ValidationError): + TypeAdapter(ItemName).validate_python([]) + + +def test_item_name_only_special_characters_is_invalid(): + with pytest.raises(ValidationError): + TypeAdapter(ItemName).validate_python("* .$") + + +def test_item_name_numeric_is_valid(): + assert TypeAdapter(ItemName).validate_python(872) == "872" + + +def test_item_str_is_valid(): + assert TypeAdapter(ItemName).validate_python("Gérard") == "Gérard" + + +def test_area_id_invalid_characters(): + with pytest.raises(ValidationError): + TypeAdapter(AreaId).validate_python("Gérard") + with pytest.raises(ValidationError): + TypeAdapter(AreaId).validate_python("Capitals") + + +def test_valid_area_id(): + assert TypeAdapter(AreaId).validate_python("area-id") == "area-id" diff --git a/tests/variantstudy/model/command/test_create_area.py b/tests/variantstudy/model/command/test_create_area.py index 3fa629f345..9c5ec995f2 100644 --- a/tests/variantstudy/model/command/test_create_area.py +++ b/tests/variantstudy/model/command/test_create_area.py @@ -16,7 +16,8 @@ from antares.study.version import StudyVersion from antarest.core.serde.ini_reader import IniReader -from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling, transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.icommand import ICommand diff --git a/tests/variantstudy/model/command/test_create_cluster.py b/tests/variantstudy/model/command/test_create_cluster.py index 9a05a29d89..3aacbfd0e0 100644 --- a/tests/variantstudy/model/command/test_create_cluster.py +++ b/tests/variantstudy/model/command/test_create_cluster.py @@ -12,19 +12,18 @@ import configparser import re -from unittest.mock import Mock import numpy as np import pytest from pydantic import ValidationError from antarest.study.model import STUDY_VERSION_8_8 -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import Thermal870Properties, ThermalClusterGroup from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_cluster import CreateCluster -from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster from antarest.study.storage.variantstudy.model.command_context import CommandContext GEN = np.random.default_rng(1000) @@ -36,8 +35,9 @@ def test_init(self, command_context: CommandContext): modulation = GEN.random((8760, 4)).tolist() cl = CreateCluster( area_id="foo", - cluster_name="Cluster1", - parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400}, + parameters=Thermal870Properties( + name="Cluster1", group=ThermalClusterGroup.NUCLEAR, unit_count=2, nominal_capacity=2400 + ), command_context=command_context, prepro=prepro, modulation=modulation, @@ -46,7 +46,6 @@ def test_init(self, command_context: CommandContext): # Check the command metadata assert cl.command_name == CommandName.CREATE_THERMAL_CLUSTER - assert cl.version == 1 assert cl.study_version == STUDY_VERSION_8_8 assert cl.command_context is command_context @@ -55,26 +54,26 @@ def test_init(self, command_context: CommandContext): modulation_id = command_context.matrix_service.create(modulation) assert cl.area_id == "foo" assert cl.cluster_name == "Cluster1" - assert cl.parameters == {"group": "Nuclear", "nominalcapacity": 2400, "unitcount": 2} + assert cl.parameters == Thermal870Properties( + name="Cluster1", group=ThermalClusterGroup.NUCLEAR, unit_count=2, nominal_capacity=2400 + ) assert cl.prepro == f"matrix://{prepro_id}" assert cl.modulation == f"matrix://{modulation_id}" def test_validate_cluster_name(self, command_context: CommandContext): - with pytest.raises(ValidationError, match="cluster_name"): + with pytest.raises(ValidationError, match="name"): CreateCluster( area_id="fr", - cluster_name="%", + parameters=Thermal870Properties(name="%"), command_context=command_context, - parameters={}, study_version=STUDY_VERSION_8_8, ) def test_validate_prepro(self, command_context: CommandContext): cl = CreateCluster( area_id="fr", - cluster_name="C1", + parameters=Thermal870Properties(name="C1"), command_context=command_context, - parameters={}, study_version=STUDY_VERSION_8_8, ) assert cl.prepro == command_context.generator_matrix_constants.get_thermal_prepro_data() @@ -82,9 +81,8 @@ def test_validate_prepro(self, command_context: CommandContext): def test_validate_modulation(self, command_context: CommandContext): cl = CreateCluster( area_id="fr", - cluster_name="C1", + parameters=Thermal870Properties(name="C1"), command_context=command_context, - parameters={}, study_version=STUDY_VERSION_8_8, ) assert cl.modulation == command_context.generator_matrix_constants.get_thermal_prepro_modulation() @@ -100,19 +98,21 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): empty_study ) - parameters = { - "group": "Other", - "unitcount": "1", - "nominalcapacity": "1000000", - "marginal-cost": "30", - "market-bid-cost": "30", - } + parameters = Thermal870Properties.model_validate( + { + "name": cluster_name, + "group": "Other", + "unitcount": "1", + "nominalcapacity": "1000000", + "marginal-cost": "30", + "market-bid-cost": "30", + } + ) prepro = GEN.random((365, 6)).tolist() modulation = GEN.random((8760, 4)).tolist() command = CreateCluster( area_id=area_id, - cluster_name=cluster_name, parameters=parameters, prepro=prepro, modulation=modulation, @@ -131,18 +131,17 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): clusters = configparser.ConfigParser() clusters.read(study_path / "input" / "thermal" / "clusters" / area_id / "list.ini") assert str(clusters[cluster_name]["name"]) == cluster_name - assert str(clusters[cluster_name]["group"]) == parameters["group"] - assert int(clusters[cluster_name]["unitcount"]) == int(parameters["unitcount"]) - assert float(clusters[cluster_name]["nominalcapacity"]) == float(parameters["nominalcapacity"]) - assert float(clusters[cluster_name]["marginal-cost"]) == float(parameters["marginal-cost"]) - assert float(clusters[cluster_name]["market-bid-cost"]) == float(parameters["market-bid-cost"]) + assert str(clusters[cluster_name]["group"]) == parameters.group + assert int(clusters[cluster_name]["unitcount"]) == parameters.unit_count + assert float(clusters[cluster_name]["nominalcapacity"]) == parameters.nominal_capacity + assert float(clusters[cluster_name]["marginal-cost"]) == parameters.marginal_cost + assert float(clusters[cluster_name]["market-bid-cost"]) == parameters.market_bid_cost assert (study_path / "input" / "thermal" / "prepro" / area_id / cluster_id / "data.txt.link").exists() assert (study_path / "input" / "thermal" / "prepro" / area_id / cluster_id / "modulation.txt.link").exists() output = CreateCluster( area_id=area_id, - cluster_name=cluster_name, parameters=parameters, prepro=prepro, modulation=modulation, @@ -158,7 +157,6 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): output = CreateCluster( area_id="non_existent_area", - cluster_name=cluster_name, parameters=parameters, prepro=prepro, modulation=modulation, @@ -177,28 +175,66 @@ def test_to_dto(self, command_context: CommandContext): modulation = GEN.random((8760, 4)).tolist() command = CreateCluster( area_id="foo", - cluster_name="Cluster1", - parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400}, + parameters=Thermal870Properties( + name="Cluster1", group=ThermalClusterGroup.NUCLEAR, unit_count=2, nominal_capacity=2400 + ), command_context=command_context, prepro=prepro, modulation=modulation, study_version=STUDY_VERSION_8_8, ) + dto = command.to_dto() + prepro_id = command_context.matrix_service.create(prepro) modulation_id = command_context.matrix_service.create(modulation) - dto = command.to_dto() assert dto.model_dump() == { "action": "create_cluster", "args": { "area_id": "foo", - "cluster_name": "Cluster1", - "parameters": {"group": "Nuclear", "nominalcapacity": 2400, "unitcount": 2}, + "parameters": { + "co2": 0.0, + "costgeneration": "SetManually", + "efficiency": 100.0, + "enabled": True, + "fixed-cost": 0.0, + "gen-ts": "use global", + "group": "nuclear", + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 0.0, + "market-bid-cost": 0.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, + "must-run": False, + "name": "Cluster1", + "nh3": 0.0, + "nmvoc": 0.0, + "nominalcapacity": 2400.0, + "nox": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0, + "pm10": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "so2": 0.0, + "spinning": 0.0, + "spread-cost": 0.0, + "startup-cost": 0.0, + "unitcount": 2, + "variableomcost": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + }, "prepro": prepro_id, "modulation": modulation_id, }, "id": None, - "version": 1, - "study_version": STUDY_VERSION_8_8, - "user_id": None, + "study_version": "8.8", "updated_at": None, + "user_id": None, + "version": 2, } diff --git a/tests/variantstudy/model/command/test_create_link.py b/tests/variantstudy/model/command/test_create_link.py index ed886e8908..56a1c92dd4 100644 --- a/tests/variantstudy/model/command/test_create_link.py +++ b/tests/variantstudy/model/command/test_create_link.py @@ -16,7 +16,7 @@ from antarest.core.exceptions import LinkValidationError from antarest.core.serde.ini_reader import IniReader from antarest.study.model import STUDY_VERSION_8_8 -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_link import CreateLink diff --git a/tests/variantstudy/model/command/test_create_renewables_cluster.py b/tests/variantstudy/model/command/test_create_renewables_cluster.py index f551721d72..36c3254851 100644 --- a/tests/variantstudy/model/command/test_create_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_create_renewables_cluster.py @@ -12,18 +12,22 @@ import configparser import re -from unittest import mock import pytest from pydantic import ValidationError from antarest.study.model import STUDY_VERSION_8_1, STUDY_VERSION_8_8 -from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling, transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( + RenewableClusterGroup, + RenewableProperties, + TimeSeriesInterpretation, +) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster -from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster from antarest.study.storage.variantstudy.model.command_context import CommandContext @@ -32,29 +36,30 @@ class TestCreateRenewablesCluster: def test_init(self, command_context: CommandContext) -> None: cl = CreateRenewablesCluster( area_id="foo", - cluster_name="Cluster1", - parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400}, + parameters=RenewableProperties( + name="Cluster1", group=RenewableClusterGroup.THERMAL_SOLAR, unit_count=2, nominal_capacity=2400 + ), command_context=command_context, study_version=STUDY_VERSION_8_8, ) # Check the command metadata assert cl.command_name == CommandName.CREATE_RENEWABLES_CLUSTER - assert cl.version == 1 assert cl.command_context is command_context # Check the command data assert cl.area_id == "foo" assert cl.cluster_name == "Cluster1" - assert cl.parameters == {"group": "Solar Thermal", "nominalcapacity": 2400, "unitcount": 2} + assert cl.parameters == RenewableProperties( + name="Cluster1", group=RenewableClusterGroup.THERMAL_SOLAR, unit_count=2, nominal_capacity=2400 + ) def test_validate_cluster_name(self, command_context: CommandContext) -> None: - with pytest.raises(ValidationError, match="cluster_name"): + with pytest.raises(ValidationError, match="Invalid name"): CreateRenewablesCluster( area_id="fr", - cluster_name="%", command_context=command_context, - parameters={}, + parameters=RenewableProperties(name="%"), study_version=STUDY_VERSION_8_8, ) @@ -69,14 +74,10 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> CreateArea(area_name=area_name, command_context=command_context, study_version=study_version).apply(empty_study) - parameters = { - "name": cluster_name, - "ts-interpretation": "power-generation", - } + parameters = RenewableProperties(name=cluster_name, ts_interpretation=TimeSeriesInterpretation.POWER_GENERATION) command = CreateRenewablesCluster( area_id=area_id, - cluster_name=cluster_name, parameters=parameters, command_context=command_context, study_version=study_version, @@ -93,11 +94,10 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> clusters = configparser.ConfigParser() clusters.read(study_path / "input" / "renewables" / "clusters" / area_id / "list.ini") assert str(clusters[cluster_name]["name"]) == cluster_name - assert str(clusters[cluster_name]["ts-interpretation"]) == parameters["ts-interpretation"] + assert str(clusters[cluster_name]["ts-interpretation"]) == "power-generation" output = CreateRenewablesCluster( area_id=area_id, - cluster_name=cluster_name, parameters=parameters, command_context=command_context, study_version=study_version, @@ -106,7 +106,6 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> output = CreateRenewablesCluster( area_id=area_id, - cluster_name=cluster_name, parameters=parameters, command_context=command_context, study_version=study_version, @@ -121,7 +120,6 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> output = CreateRenewablesCluster( area_id="non_existent_area", - cluster_name=cluster_name, parameters=parameters, command_context=command_context, study_version=study_version, @@ -137,8 +135,9 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> def test_to_dto(self, command_context: CommandContext) -> None: command = CreateRenewablesCluster( area_id="foo", - cluster_name="Cluster1", - parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400}, + parameters=RenewableProperties( + name="Cluster1", group=RenewableClusterGroup.THERMAL_SOLAR, unit_count=2, nominal_capacity=2400 + ), command_context=command_context, study_version=STUDY_VERSION_8_8, ) @@ -147,12 +146,18 @@ def test_to_dto(self, command_context: CommandContext) -> None: "action": "create_renewables_cluster", # "renewables" with a final "s". "args": { "area_id": "foo", - "cluster_name": "Cluster1", - "parameters": {"group": "Solar Thermal", "nominalcapacity": 2400, "unitcount": 2}, + "parameters": { + "name": "Cluster1", + "group": "solar thermal", + "nominalcapacity": 2400, + "unitcount": 2, + "enabled": True, + "ts-interpretation": "power-generation", + }, }, "id": None, - "version": 1, - "study_version": STUDY_VERSION_8_8, + "version": 2, + "study_version": "8.8", "updated_at": None, "user_id": None, } diff --git a/tests/variantstudy/model/command/test_create_st_storage.py b/tests/variantstudy/model/command/test_create_st_storage.py index 32ab04ec5a..d07ea8e999 100644 --- a/tests/variantstudy/model/command/test_create_st_storage.py +++ b/tests/variantstudy/model/command/test_create_st_storage.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import copy import re import numpy as np @@ -17,8 +17,13 @@ from pydantic import ValidationError from antarest.study.model import STUDY_VERSION_8_8 -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( + STStorage880Properties, + STStorageConfig, + STStorageGroup, + STStorageProperties, +) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.study_upgrader import StudyUpgrader from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol @@ -81,7 +86,7 @@ def test_init(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), pmax_injection=pmax_injection.tolist(), # type: ignore inflows=inflows.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, @@ -89,12 +94,11 @@ def test_init(self, command_context: CommandContext): # Check the attribues assert cmd.command_name == CommandName.CREATE_ST_STORAGE - assert cmd.version == 1 assert cmd.study_version == STUDY_VERSION_8_8 assert cmd.command_context == command_context assert cmd.area_id == "area_fr" expected_parameters = {k: str(v) for k, v in PARAMETERS.items()} - assert cmd.parameters == STStorageConfig(**expected_parameters) + assert cmd.parameters == STStorageProperties(**expected_parameters) # check the matrices links @@ -105,6 +109,18 @@ def test_init(self, command_context: CommandContext): assert cmd.upper_rule_curve == constants.get_st_storage_upper_rule_curve() assert cmd.inflows != constants.get_st_storage_inflows() + @pytest.mark.parametrize("group", ["Battery", "battery"]) + def test_init__lower_and_upper_case_groups_are_valid(self, command_context: CommandContext, group: str): + params = copy.deepcopy(PARAMETERS) + params["group"] = group + cmd = CreateSTStorage( + command_context=command_context, + area_id="area_fr", + parameters=STStorage880Properties(**PARAMETERS), + study_version=STUDY_VERSION_8_8, + ) + assert cmd.parameters.group == STStorageGroup.BATTERY + def test_init__invalid_storage_name(self, recent_study: FileStudy, command_context: CommandContext): # When we apply the config for a new ST Storage with a bad name with pytest.raises(ValidationError) as ctx: @@ -112,7 +128,7 @@ def test_init__invalid_storage_name(self, recent_study: FileStudy, command_conte CreateSTStorage( command_context=command_context, area_id="dummy", - parameters=STStorageConfig(**parameters), + parameters=STStorageProperties(**parameters), study_version=STUDY_VERSION_8_8, ) # We get 2 errors because the `storage_name` is duplicated in the `parameters`: @@ -120,15 +136,7 @@ def test_init__invalid_storage_name(self, recent_study: FileStudy, command_conte raised_error = ctx.value.errors()[0] assert raised_error["type"] == "value_error" assert raised_error["msg"] == "Value error, Invalid name '?%$$'." - assert raised_error["input"] == { - "efficiency": 0.94, - "group": "Battery", - "initialleveloptim": True, - "injectionnominalcapacity": 1500, - "name": "?%$$", - "reservoircapacity": 20000, - "withdrawalnominalcapacity": 1500, - } + assert raised_error["input"] == "?%$$" def test_init__invalid_matrix_values(self, command_context: CommandContext): array = GEN.random((8760, 1)) @@ -137,7 +145,7 @@ def test_init__invalid_matrix_values(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), pmax_injection=array.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, ) @@ -154,7 +162,7 @@ def test_init__invalid_matrix_shape(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), pmax_injection=array.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, ) @@ -171,7 +179,7 @@ def test_init__invalid_nan_value(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), pmax_injection=array.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, ) @@ -186,7 +194,7 @@ def test_init__invalid_matrix_type(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), pmax_injection=[1, 2, 3], study_version=STUDY_VERSION_8_8, ) @@ -202,7 +210,7 @@ def test_apply_config__invalid_version(self, empty_study: FileStudy, command_con create_st_storage = CreateSTStorage( command_context=command_context, area_id="foo", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=empty_study.config.version, ) command_output = create_st_storage.apply_config(empty_study.config) @@ -221,7 +229,7 @@ def test_apply_config__missing_area(self, recent_study: FileStudy, command_conte create_st_storage = CreateSTStorage( command_context=command_context, area_id="unknown area", # bad ID - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -245,7 +253,7 @@ def test_apply_config__duplicate_storage(self, recent_study: FileStudy, command_ create_st_storage = CreateSTStorage( command_context=command_context, area_id=transform_name_to_id(create_area.area_name), - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -256,7 +264,7 @@ def test_apply_config__duplicate_storage(self, recent_study: FileStudy, command_ create_st_storage = CreateSTStorage( command_context=command_context, area_id=transform_name_to_id(create_area.area_name), - parameters=STStorageConfig(**parameters), + parameters=STStorageProperties(**parameters), study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -280,7 +288,7 @@ def test_apply_config__nominal_case(self, recent_study: FileStudy, command_conte create_st_storage = CreateSTStorage( command_context=command_context, area_id=transform_name_to_id(create_area.area_name), - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -307,7 +315,7 @@ def test_apply__nominal_case(self, recent_study: FileStudy, command_context: Com cmd = CreateSTStorage( command_context=command_context, area_id=transform_name_to_id(create_area.area_name), - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), pmax_injection=pmax_injection.tolist(), # type: ignore inflows=inflows.tolist(), # type: ignore study_version=recent_study.config.version, @@ -320,7 +328,7 @@ def test_apply__nominal_case(self, recent_study: FileStudy, command_context: Com expected = { "storage1": { "efficiency": 0.94, - "group": "Battery", + "group": "battery", "initiallevel": 0.5, "initialleveloptim": True, "injectionnominalcapacity": 1500, @@ -359,7 +367,7 @@ def test_apply__invalid_apply_config(self, empty_study: FileStudy, command_conte cmd = CreateSTStorage( command_context=command_context, area_id=transform_name_to_id(create_area.area_name), - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=empty_study.config.version, ) command_output = cmd.apply(empty_study) @@ -370,7 +378,7 @@ def test_to_dto(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=STUDY_VERSION_8_8, ) @@ -379,10 +387,12 @@ def test_to_dto(self, command_context: CommandContext): expected_parameters = PARAMETERS.copy() # `initiallevel` = 0.5 (the default value) because `initialleveloptim` is True expected_parameters["initiallevel"] = 0.5 + expected_parameters["group"] = "battery" constants = command_context.generator_matrix_constants assert actual == CommandDTO( action=CommandName.CREATE_ST_STORAGE.value, + version=2, args={ "area_id": "area_fr", "parameters": expected_parameters, @@ -399,7 +409,7 @@ def test_get_inner_matrices(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=STStorageProperties(**PARAMETERS), study_version=STUDY_VERSION_8_8, ) actual = cmd.get_inner_matrices() diff --git a/tests/variantstudy/model/command/test_manage_binding_constraints.py b/tests/variantstudy/model/command/test_manage_binding_constraints.py index e70ffc29a9..96fa9ee829 100644 --- a/tests/variantstudy/model/command/test_manage_binding_constraints.py +++ b/tests/variantstudy/model/command/test_manage_binding_constraints.py @@ -17,7 +17,8 @@ BindingConstraintFrequency, BindingConstraintOperator, ) -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalProperties from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.matrix_constants.binding_constraint.series_after_v87 import ( default_bc_weekly_daily as default_bc_weekly_daily_870, @@ -53,7 +54,10 @@ def test_manage_binding_constraint(empty_study: FileStudy, command_context: Comm empty_study ) CreateCluster( - area_id=area1, cluster_name=cluster, parameters={}, command_context=command_context, study_version=study_version + area_id=area1, + parameters=ThermalProperties(name=cluster), + command_context=command_context, + study_version=study_version, ).apply(empty_study) output = CreateBindingConstraint( @@ -328,7 +332,10 @@ def test__update_matrices_names( empty_study ) CreateCluster( - area_id=area1, cluster_name=cluster, parameters={}, command_context=command_context, study_version=study_version + area_id=area1, + parameters=ThermalProperties(name=cluster), + command_context=command_context, + study_version=study_version, ).apply(empty_study) # create a binding constraint diff --git a/tests/variantstudy/model/command/test_manage_district.py b/tests/variantstudy/model/command/test_manage_district.py index 8e10c40d57..549c1e4d0d 100644 --- a/tests/variantstudy/model/command/test_manage_district.py +++ b/tests/variantstudy/model/command/test_manage_district.py @@ -12,7 +12,7 @@ from antarest.core.serde.ini_reader import IniReader from antarest.study.storage.rawstudy.model.filesystem.config.files import build -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_district import CreateDistrict, DistrictBaseFilter diff --git a/tests/variantstudy/model/command/test_remove_area.py b/tests/variantstudy/model/command/test_remove_area.py index a29cd693ec..bdc92c36bb 100644 --- a/tests/variantstudy/model/command/test_remove_area.py +++ b/tests/variantstudy/model/command/test_remove_area.py @@ -13,12 +13,15 @@ import pytest from checksumdir import dirhash +from antarest.study.business.areas.renewable_management import TimeSeriesInterpretation from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import ( BindingConstraintFrequency, BindingConstraintOperator, ) -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import RenewableClusterGroup, RenewableProperties +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalClusterGroup, ThermalProperties from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_binding_constraint import CreateBindingConstraint @@ -144,14 +147,14 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): thermal_id = transform_name_to_id(thermal_name) output = CreateCluster( area_id=area_id2, - cluster_name=thermal_name, - parameters={ - "group": "Other", - "unitcount": "1", - "nominalcapacity": "1000000", - "marginal-cost": "30", - "market-bid-cost": "30", - }, + parameters=ThermalProperties( + name=thermal_name, + group=ThermalClusterGroup.OTHER1, + unit_count=1, + nominal_capacity=1000000, + marginal_cost=30, + market_bid_cost=30, + ), prepro=[[0]], modulation=[[0]], command_context=command_context, @@ -165,14 +168,14 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): renewable_id = transform_name_to_id(renewable_name) output = CreateRenewablesCluster( area_id=area_id2, - cluster_name=renewable_name, - parameters={ - "enabled": "true", - "group": "Solar Rooftop", - "unitcount": "10", - "nominalcapacity": "12000", - "ts-interpretation": "power-generation", - }, + parameters=RenewableProperties( + name=renewable_name, + enabled=True, + group=RenewableClusterGroup.ROOFTOP_SOLAR, + unit_count=10, + nominal_capacity=12000, + ts_interpretation=TimeSeriesInterpretation.POWER_GENERATION, + ), command_context=command_context, study_version=study_version, ).apply(study_data=empty_study) diff --git a/tests/variantstudy/model/command/test_remove_cluster.py b/tests/variantstudy/model/command/test_remove_cluster.py index 266cf63a86..baca7d65c1 100644 --- a/tests/variantstudy/model/command/test_remove_cluster.py +++ b/tests/variantstudy/model/command/test_remove_cluster.py @@ -18,7 +18,13 @@ BindingConstraintFrequency, BindingConstraintOperator, ) -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorage880Properties +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( + Thermal870Properties, + ThermalClusterGroup, + ThermalProperties, +) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_binding_constraint import CreateBindingConstraint @@ -50,21 +56,22 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> reset_line_separator(empty_study.config.study_path.joinpath("settings/scenariobuilder.dat")) hash_before_removal = dirhash(empty_study.config.study_path, "md5") - CreateCluster( + output = CreateCluster( area_id=area_id, - cluster_name=cluster_name, - parameters={ - "group": "group", - "unitcount": "unitcount", - "nominalcapacity": "nominalcapacity", - "marginal-cost": "marginal-cost", - "market-bid-cost": "market-bid-cost", - }, + parameters=ThermalProperties( + name=cluster_name, + group=ThermalClusterGroup.NUCLEAR, + unit_count=1, + nominal_capacity=100, + marginal_cost=40, + market_bid_cost=40, + ), command_context=command_context, prepro=[[0]], modulation=[[0]], study_version=study_version, ).apply(empty_study) + assert output.status, output.message # Binding constraint 2nd member: array of shape (8784, 3) array = np.random.rand(8784, 3) * 1000 diff --git a/tests/variantstudy/model/command/test_remove_link.py b/tests/variantstudy/model/command/test_remove_link.py index e65934738f..066a5fb124 100644 --- a/tests/variantstudy/model/command/test_remove_link.py +++ b/tests/variantstudy/model/command/test_remove_link.py @@ -23,7 +23,7 @@ from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.config.files import build -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/model/command/test_remove_renewables_cluster.py b/tests/variantstudy/model/command/test_remove_renewables_cluster.py index c5bba2a4d7..c9c14d508f 100644 --- a/tests/variantstudy/model/command/test_remove_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_remove_renewables_cluster.py @@ -12,7 +12,12 @@ from antares.study.version import StudyVersion from checksumdir import dirhash -from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling, transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.model import EnrModelling +from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( + RenewableProperties, + TimeSeriesInterpretation, +) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster @@ -45,11 +50,9 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> CreateRenewablesCluster( area_id=area_id, - cluster_name=cluster_id, - parameters={ - "name": cluster_name, - "ts-interpretation": "power-generation", - }, + parameters=RenewableProperties( + name=cluster_name, ts_interpretation=TimeSeriesInterpretation.POWER_GENERATION + ), command_context=command_context, study_version=study_version, ).apply(empty_study) diff --git a/tests/variantstudy/model/command/test_remove_st_storage.py b/tests/variantstudy/model/command/test_remove_st_storage.py index 62d911f2bb..f9e76a643a 100644 --- a/tests/variantstudy/model/command/test_remove_st_storage.py +++ b/tests/variantstudy/model/command/test_remove_st_storage.py @@ -16,7 +16,7 @@ from pydantic import ValidationError from antarest.study.model import STUDY_VERSION_8_8 -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.study_upgrader import StudyUpgrader from antarest.study.storage.variantstudy.model.command.common import CommandName @@ -66,7 +66,6 @@ def test_init(self, command_context: CommandContext): # Check the attribues assert cmd.command_name == CommandName.REMOVE_ST_STORAGE - assert cmd.version == 1 assert cmd.study_version == STUDY_VERSION_8_8 assert cmd.command_context == command_context assert cmd.area_id == "area_fr" diff --git a/tests/variantstudy/model/command/test_replace_matrix.py b/tests/variantstudy/model/command/test_replace_matrix.py index d54ba755e2..c635071fab 100644 --- a/tests/variantstudy/model/command/test_replace_matrix.py +++ b/tests/variantstudy/model/command/test_replace_matrix.py @@ -15,7 +15,7 @@ import numpy as np from antarest.study.model import STUDY_VERSION_8_8 -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix diff --git a/tests/variantstudy/model/command/test_update_config.py b/tests/variantstudy/model/command/test_update_config.py index b87583914a..f41c4e372a 100644 --- a/tests/variantstudy/model/command/test_update_config.py +++ b/tests/variantstudy/model/command/test_update_config.py @@ -15,7 +15,7 @@ import pytest from antarest.core.serde.ini_reader import IniReader -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 10587a0c7b..5e666660cd 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -13,7 +13,7 @@ import importlib import itertools import pkgutil -from typing import List, Set +from typing import Any, Dict, List, Optional, Set from unittest.mock import Mock import pytest @@ -28,410 +28,727 @@ from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.model import CommandDTO -COMMANDS: List[CommandDTO] = [ - CommandDTO(action=CommandName.CREATE_AREA.value, args={"area_name": "area_name"}, study_version=STUDY_VERSION_8_8), - CommandDTO( - action=CommandName.CREATE_AREA.value, - args=[ - {"area_name": "area_name"}, - {"area_name": "area2"}, - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO(action=CommandName.REMOVE_AREA.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), - CommandDTO(action=CommandName.REMOVE_AREA.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), - CommandDTO( - action=CommandName.UPDATE_AREA_UI.value, - args={ - "area_id": "id", - "area_ui": UpdateAreaUi(x=100, y=100, color_rgb=(100, 100, 100), layer_x={}, layer_y={}, layer_color={}), - "layer": "0", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_DISTRICT.value, - args={ - "name": "id", - "filter_items": ["a"], - "output": True, - "comments": "", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_DISTRICT.value, - args=[ - { +# First: input DTO +# Second: expected args after round trip or None if expecting same as input args +COMMANDS = [ + pytest.param( + CommandDTO( + action=CommandName.CREATE_AREA.value, args={"area_name": "area_name"}, study_version=STUDY_VERSION_8_8 + ), + None, + id="create_area", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_AREA.value, + args=[ + {"area_name": "area_name"}, + {"area_name": "area2"}, + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_area2", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_AREA_UI.value, + args={ + "area_id": "id", + "area_ui": UpdateAreaUi( + x=100, y=100, color_rgb=(100, 100, 100), layer_x={}, layer_y={}, layer_color={} + ), + "layer": "0", + }, + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_area_ui", + ), + pytest.param( + CommandDTO(action=CommandName.REMOVE_AREA.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), + None, + id="remove_area", + ), + pytest.param( + CommandDTO(action=CommandName.REMOVE_AREA.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), + None, + id="remove_area2", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_DISTRICT.value, + args={ "name": "id", - "base_filter": "add-all", + "filter_items": ["a"], "output": True, "comments": "", - } - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO(action=CommandName.REMOVE_DISTRICT.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), - CommandDTO(action=CommandName.REMOVE_DISTRICT.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), - CommandDTO( - action=CommandName.CREATE_LINK.value, - args={ - "area1": "area1", - "area2": "area2", - "parameters": {}, - "series": "series", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_LINK.value, - args=[ - { - "area1": "area1", - "area2": "area2", - "parameters": {}, - "series": "series", - } - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_LINK.value, - args=[ - { + }, + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_district", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_DISTRICT.value, + args=[ + { + "name": "id", + "base_filter": "add-all", + "output": True, + "comments": "", + } + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_district_list", + ), + pytest.param( + CommandDTO(action=CommandName.REMOVE_DISTRICT.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), + None, + id="remove_district", + ), + pytest.param( + CommandDTO(action=CommandName.REMOVE_DISTRICT.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), + None, + id="remove_district_list", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_LINK.value, + args={ "area1": "area1", "area2": "area2", "parameters": {}, "series": "series", - } - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_LINK.value, - args={ - "area1": "area1", - "area2": "area2", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_LINK.value, - args=[ - { + }, + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_link", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_LINK.value, + args=[ + { + "area1": "area1", + "area2": "area2", + "parameters": {}, + "series": "series", + } + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_link_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_LINK.value, + args=[ + { + "area1": "area1", + "area2": "area2", + "parameters": {}, + "series": "series", + } + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_link", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_LINK.value, + args={ "area1": "area1", "area2": "area2", - } - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_BINDING_CONSTRAINT.value, args={"name": "name"}, study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.CREATE_BINDING_CONSTRAINT.value, - args=[ - { - "name": "name", - "enabled": True, - "time_step": "hourly", - "operator": "equal", - "values": "values", - "group": "group_1", }, - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_BINDING_CONSTRAINT.value, - args={ - "id": "id", - "enabled": True, - "time_step": "hourly", - "operator": "equal", - "values": "values", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_BINDING_CONSTRAINT.value, - args=[ - { + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_link", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_LINK.value, + args=[ + { + "area1": "area1", + "area2": "area2", + } + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_link_list", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_BINDING_CONSTRAINT.value, args={"name": "name"}, study_version=STUDY_VERSION_8_8 + ), + None, + id="create_binding_constraint", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_BINDING_CONSTRAINT.value, + args=[ + { + "name": "name", + "enabled": True, + "time_step": "hourly", + "operator": "equal", + "values": "values", + "group": "group_1", + }, + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_binding_constraint_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_BINDING_CONSTRAINT.value, + args={ "id": "id", "enabled": True, "time_step": "hourly", "operator": "equal", - } - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO(action=CommandName.REMOVE_BINDING_CONSTRAINT.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), - CommandDTO( - action=CommandName.REMOVE_BINDING_CONSTRAINT.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value, - args={"ids": ["id"]}, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value, - args=[{"ids": ["id"]}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_THERMAL_CLUSTER.value, - args={ - "area_id": "area_name", - "cluster_name": "cluster_name", - "parameters": { - "group": "group", - "unitcount": "unitcount", - "nominalcapacity": "nominalcapacity", - "marginal-cost": "marginal-cost", - "market-bid-cost": "market-bid-cost", + "values": "values", }, - "prepro": "prepro", - "modulation": "modulation", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_THERMAL_CLUSTER.value, - args=[ + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_binding_constraint", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_BINDING_CONSTRAINT.value, + args=[ + { + "id": "id", + "enabled": True, + "time_step": "hourly", + "operator": "equal", + } + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="udpate_binding_constraint_list", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_BINDING_CONSTRAINT.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8 + ), + None, + id="remove_binding_constraint", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_BINDING_CONSTRAINT.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8 + ), + None, + id="remove_binding_constraint_list", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value, + args={"ids": ["id"]}, + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_multiple_constraints", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value, + args=[{"ids": ["id"]}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_multiple_constraints_list", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_THERMAL_CLUSTER.value, + version=2, + args=[ + { + "area_id": "area_name", + "parameters": { + "name": "cluster_name", + "group": "nuclear", + "unitcount": 3, + "nominalcapacity": 100, + "marginal-cost": 40, + "market-bid-cost": 45, + }, + "prepro": "prepro", + "modulation": "modulation", + } + ], + study_version=STUDY_VERSION_8_8, + ), + [ { "area_id": "area_name", - "cluster_name": "cluster_name", + "modulation": "modulation", "parameters": { - "group": "group", - "unitcount": "unitcount", - "nominalcapacity": "nominalcapacity", - "marginal-cost": "marginal-cost", - "market-bid-cost": "market-bid-cost", + "co2": 0.0, + "costgeneration": "SetManually", + "efficiency": 100.0, + "enabled": True, + "fixed-cost": 0.0, + "gen-ts": "use global", + "group": "nuclear", + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 40.0, + "market-bid-cost": 45.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, + "must-run": False, + "name": "cluster_name", + "nh3": 0.0, + "nmvoc": 0.0, + "nominalcapacity": 100.0, + "nox": 0.0, + "op1": 0.0, + "op2": 0.0, + "op3": 0.0, + "op4": 0.0, + "op5": 0.0, + "pm10": 0.0, + "pm2_5": 0.0, + "pm5": 0.0, + "so2": 0.0, + "spinning": 0.0, + "spread-cost": 0.0, + "startup-cost": 0.0, + "unitcount": 3, + "variableomcost": 0.0, + "volatility.forced": 0.0, + "volatility.planned": 0.0, }, "prepro": "prepro", - "modulation": "modulation", } ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_THERMAL_CLUSTER.value, - args={"area_id": "area_name", "cluster_id": "cluster_name"}, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_THERMAL_CLUSTER.value, - args=[{"area_id": "area_name", "cluster_id": "cluster_name"}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_RENEWABLES_CLUSTER.value, - args={ + id="create_thermal_cluster_list", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_THERMAL_CLUSTER.value, + args={"area_id": "area_name", "cluster_id": "cluster_name"}, + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_thermal_cluster", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_THERMAL_CLUSTER.value, + args=[{"area_id": "area_name", "cluster_id": "cluster_name"}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_thermal_cluster_list", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_RENEWABLES_CLUSTER.value, + version=2, + args={ + "area_id": "area_name", + "parameters": { + "name": "cluster_name", + "ts-interpretation": "power-generation", + }, + }, + study_version=STUDY_VERSION_8_8, + ), + { "area_id": "area_name", - "cluster_name": "cluster_name", "parameters": { - "name": "name", + "enabled": True, + "group": "other res 1", + "name": "cluster_name", + "nominalcapacity": 0.0, "ts-interpretation": "power-generation", + "unitcount": 1, }, }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_RENEWABLES_CLUSTER.value, - args=[ + id="create_renewables_cluster", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_RENEWABLES_CLUSTER.value, + version=2, + args=[ + { + "area_id": "area_name", + "parameters": { + "name": "cluster_name", + "ts-interpretation": "power-generation", + }, + } + ], + study_version=STUDY_VERSION_8_8, + ), + [ { "area_id": "area_name", - "cluster_name": "cluster_name", "parameters": { - "name": "name", + "enabled": True, + "group": "other res 1", + "name": "cluster_name", + "nominalcapacity": 0.0, "ts-interpretation": "power-generation", + "unitcount": 1, }, } ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_RENEWABLES_CLUSTER.value, - args={"area_id": "area_name", "cluster_id": "cluster_name"}, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_RENEWABLES_CLUSTER.value, - args=[{"area_id": "area_name", "cluster_id": "cluster_name"}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REPLACE_MATRIX.value, - args={"target": "target_element", "matrix": "matrix"}, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REPLACE_MATRIX.value, - args=[{"target": "target_element", "matrix": "matrix"}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_CONFIG.value, args={"target": "target", "data": {}}, study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.UPDATE_CONFIG.value, args=[{"target": "target", "data": {}}], study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.UPDATE_COMMENTS.value, args={"comments": "comments"}, study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.UPDATE_COMMENTS.value, args=[{"comments": "comments"}], study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.UPDATE_FILE.value, - args={ - "target": "settings/resources/study", - "b64Data": "", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_DISTRICT.value, - args={"id": "id", "filter_items": ["a"]}, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_DISTRICT.value, - args=[{"id": "id", "base_filter": "add-all"}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_PLAYLIST.value, - args=[{"active": True, "items": [1, 3], "reverse": False}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_PLAYLIST.value, - args={ - "active": True, - "items": [1, 3], - "weights": {1: 5.0}, - "reverse": False, - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.UPDATE_SCENARIO_BUILDER.value, - args={ - "data": { - "ruleset test": { - "l": {"area1": {"0": 1}}, - "ntc": {"area1 / area2": {"1": 23}}, - "t": {"area1": {"thermal": {"1": 2}}}, + id="create_renewables_cluster_list", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_RENEWABLES_CLUSTER.value, + args={"area_id": "area_name", "cluster_id": "cluster_name"}, + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_renewables_cluster", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_RENEWABLES_CLUSTER.value, + args=[{"area_id": "area_name", "cluster_id": "cluster_name"}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_renewables_cluster_list", + ), + pytest.param( + CommandDTO( + action=CommandName.REPLACE_MATRIX.value, + args={"target": "target_element", "matrix": "matrix"}, + study_version=STUDY_VERSION_8_8, + ), + None, + id="replace_matrix", + ), + pytest.param( + CommandDTO( + action=CommandName.REPLACE_MATRIX.value, + args=[{"target": "target_element", "matrix": "matrix"}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="replace_matrix_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_CONFIG.value, + args={"target": "target", "data": {}}, + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_config", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_CONFIG.value, + args=[{"target": "target", "data": {}}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_config_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_COMMENTS.value, args={"comments": "comments"}, study_version=STUDY_VERSION_8_8 + ), + None, + id="update_comments", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_COMMENTS.value, args=[{"comments": "comments"}], study_version=STUDY_VERSION_8_8 + ), + None, + id="update_comments_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_FILE.value, + args={ + "target": "settings/resources/study", + "b64Data": "", + }, + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_file", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_DISTRICT.value, + args={"id": "id", "filter_items": ["a"]}, + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_district", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_DISTRICT.value, + args=[{"id": "id", "base_filter": "add-all"}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_district_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_PLAYLIST.value, + args=[{"active": True, "items": [1, 3], "reverse": False}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_playlist_list", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_PLAYLIST.value, + args={ + "active": True, + "items": [1, 3], + "weights": {1: 5.0}, + "reverse": False, + }, + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_playlist", + ), + pytest.param( + CommandDTO( + action=CommandName.UPDATE_SCENARIO_BUILDER.value, + args={ + "data": { + "ruleset test": { + "l": {"area1": {"0": 1}}, + "ntc": {"area1 / area2": {"1": 23}}, + "t": {"area1": {"thermal": {"1": 2}}}, + }, + } + }, + study_version=STUDY_VERSION_8_8, + ), + None, + id="update_scenario_builder", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_ST_STORAGE.value, + version=2, + args={ + "area_id": "area 1", + "parameters": { + "name": "Storage 1", + "group": "Battery", + "injectionnominalcapacity": 0, + "withdrawalnominalcapacity": 0, + "reservoircapacity": 0, + "efficiency": 1, + "initiallevel": 0, + "initialleveloptim": False, }, - } - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_ST_STORAGE.value, - args={ + "pmax_injection": "matrix://59ea6c83-6348-466d-9530-c35c51ca4c37", + "pmax_withdrawal": "matrix://5f988548-dadc-4bbb-8ce8-87a544dbf756", + "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", + "upper_rule_curve": "matrix://8ce614c8-c687-41af-8b24-df8a49cc52af", + "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", + }, + study_version=STUDY_VERSION_8_8, + ), + { "area_id": "area 1", + "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", + "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", "parameters": { - "name": "Storage 1", - "group": "Battery", - "injectionnominalcapacity": 0, - "withdrawalnominalcapacity": 0, - "reservoircapacity": 0, - "efficiency": 1, - "initiallevel": 0, + "efficiency": 1.0, + "enabled": True, + "group": "battery", + "initiallevel": 0.0, "initialleveloptim": False, + "injectionnominalcapacity": 0.0, + "name": "Storage 1", + "reservoircapacity": 0.0, + "withdrawalnominalcapacity": 0.0, }, "pmax_injection": "matrix://59ea6c83-6348-466d-9530-c35c51ca4c37", "pmax_withdrawal": "matrix://5f988548-dadc-4bbb-8ce8-87a544dbf756", - "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", "upper_rule_curve": "matrix://8ce614c8-c687-41af-8b24-df8a49cc52af", - "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.CREATE_ST_STORAGE.value, - args=[ + id="create_st_storage", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_ST_STORAGE.value, + version=2, + args=[ + { + "area_id": "area 1", + "parameters": { + "efficiency": 1, + "group": "Battery", + "initiallevel": 0, + "initialleveloptim": False, + "injectionnominalcapacity": 0, + "name": "Storage 1", + "reservoircapacity": 0, + "withdrawalnominalcapacity": 0, + }, + "pmax_injection": "matrix://59ea6c83-6348-466d-9530-c35c51ca4c37", + "pmax_withdrawal": "matrix://5f988548-dadc-4bbb-8ce8-87a544dbf756", + "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", + "upper_rule_curve": "matrix://8ce614c8-c687-41af-8b24-df8a49cc52af", + "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", + }, + { + "area_id": "area 1", + "parameters": { + "efficiency": 0.94, + "group": "Battery", + "initiallevel": 0, + "initialleveloptim": False, + "injectionnominalcapacity": 0, + "name": "Storage 2", + "reservoircapacity": 0, + "withdrawalnominalcapacity": 0, + }, + "pmax_injection": "matrix://3f5b3746-3995-49b7-a6da-622633472e05", + "pmax_withdrawal": "matrix://4b64a31f-927b-4887-b4cd-adcddd39bdcd", + "lower_rule_curve": "matrix://16c7c3ae-9824-4ef2-aa68-51145884b025", + "upper_rule_curve": "matrix://9a6104e9-990a-415f-a6e2-57507e13b58c", + "inflows": "matrix://e8923768-9bdd-40c2-a6ea-2da2523be727", + }, + ], + study_version=STUDY_VERSION_8_8, + ), + [ { "area_id": "area 1", + "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", + "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", "parameters": { - "efficiency": 1, - "group": "Battery", - "initiallevel": 0, + "efficiency": 1.0, + "enabled": True, + "group": "battery", + "initiallevel": 0.0, "initialleveloptim": False, - "injectionnominalcapacity": 0, + "injectionnominalcapacity": 0.0, "name": "Storage 1", - "reservoircapacity": 0, - "withdrawalnominalcapacity": 0, + "reservoircapacity": 0.0, + "withdrawalnominalcapacity": 0.0, }, "pmax_injection": "matrix://59ea6c83-6348-466d-9530-c35c51ca4c37", "pmax_withdrawal": "matrix://5f988548-dadc-4bbb-8ce8-87a544dbf756", - "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", "upper_rule_curve": "matrix://8ce614c8-c687-41af-8b24-df8a49cc52af", - "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", }, { "area_id": "area 1", + "inflows": "matrix://e8923768-9bdd-40c2-a6ea-2da2523be727", + "lower_rule_curve": "matrix://16c7c3ae-9824-4ef2-aa68-51145884b025", "parameters": { "efficiency": 0.94, - "group": "Battery", - "initiallevel": 0, + "enabled": True, + "group": "battery", + "initiallevel": 0.0, "initialleveloptim": False, - "injectionnominalcapacity": 0, + "injectionnominalcapacity": 0.0, "name": "Storage 2", - "reservoircapacity": 0, - "withdrawalnominalcapacity": 0, + "reservoircapacity": 0.0, + "withdrawalnominalcapacity": 0.0, }, "pmax_injection": "matrix://3f5b3746-3995-49b7-a6da-622633472e05", "pmax_withdrawal": "matrix://4b64a31f-927b-4887-b4cd-adcddd39bdcd", - "lower_rule_curve": "matrix://16c7c3ae-9824-4ef2-aa68-51145884b025", "upper_rule_curve": "matrix://9a6104e9-990a-415f-a6e2-57507e13b58c", - "inflows": "matrix://e8923768-9bdd-40c2-a6ea-2da2523be727", }, ], - study_version=STUDY_VERSION_8_8, + id="create_st_storage_list", ), - CommandDTO( - action=CommandName.REMOVE_ST_STORAGE.value, - args={ - "area_id": "area 1", - "storage_id": "storage 1", - }, - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_ST_STORAGE.value, - args=[ - { + pytest.param( + CommandDTO( + action=CommandName.REMOVE_ST_STORAGE.value, + args={ "area_id": "area 1", "storage_id": "storage 1", }, - { - "area_id": "area 1", - "storage_id": "storage 2", - }, - ], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.GENERATE_THERMAL_CLUSTER_TIMESERIES.value, args=[{}], study_version=STUDY_VERSION_8_8 - ), - CommandDTO( - action=CommandName.CREATE_USER_RESOURCE.value, - args=[{"data": {"path": "folder_1", "resource_type": "folder"}}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_USER_RESOURCE.value, - args=[{"data": {"path": "folder_1"}}], - study_version=STUDY_VERSION_8_8, - ), - CommandDTO( - action=CommandName.REMOVE_USER_RESOURCE.value, - args=[{"data": {"path": "file_1.txt"}}], - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_st_storage", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_ST_STORAGE.value, + args=[ + { + "area_id": "area 1", + "storage_id": "storage 1", + }, + { + "area_id": "area 1", + "storage_id": "storage 2", + }, + ], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_st_storage_list", + ), + pytest.param( + CommandDTO( + action=CommandName.GENERATE_THERMAL_CLUSTER_TIMESERIES.value, args=[{}], study_version=STUDY_VERSION_8_8 + ), + None, + id="generate_thermal_cluster_timeseries_list", + ), + pytest.param( + CommandDTO( + action=CommandName.CREATE_USER_RESOURCE.value, + args=[{"data": {"path": "folder_1", "resource_type": "folder"}}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="create_user_resource_list", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_USER_RESOURCE.value, + args=[{"data": {"path": "folder_1"}}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_user_resource_list_folder", + ), + pytest.param( + CommandDTO( + action=CommandName.REMOVE_USER_RESOURCE.value, + args=[{"data": {"path": "file_1.txt"}}], + study_version=STUDY_VERSION_8_8, + ), + None, + id="remove_user_resource_list_file", ), ] @@ -463,25 +780,22 @@ def _get_command_classes(self) -> Set[str]: abstract_commands = {"AbstractBindingConstraintCommand", "AbstractLinkCommand"} return {cmd.__name__ for cmd in ICommand.__subclasses__() if cmd.__name__ not in abstract_commands} - def test_all_commands_are_tested(self, command_factory: CommandFactory): - commands = sum([command_factory.to_command(command_dto=cmd) for cmd in COMMANDS], []) - tested_classes = {c.__class__.__name__ for c in commands} - - assert self._get_command_classes().issubset(tested_classes) - # noinspection SpellCheckingInspection @pytest.mark.parametrize( - "command_dto", + ["command_dto", "expected_args"], COMMANDS, ) @pytest.mark.unit_test - def test_command_factory(self, command_dto: CommandDTO, command_factory: CommandFactory): + def test_command_factory( + self, command_dto: CommandDTO, expected_args: Optional[Dict[str, Any]], command_factory: CommandFactory + ): commands = command_factory.to_command(command_dto=command_dto) - if isinstance(command_dto.args, dict): - exp_action_args_list = [(command_dto.action, command_dto.args, command_dto.version)] + expected_args = expected_args or command_dto.args + if isinstance(expected_args, dict): + exp_action_args_list = [(command_dto.action, expected_args, command_dto.version)] else: - exp_action_args_list = [(command_dto.action, args, command_dto.version) for args in command_dto.args] + exp_action_args_list = [(command_dto.action, args, command_dto.version) for args in expected_args] actual_cmd: ICommand for actual_cmd, exp_action_args_version in itertools.zip_longest(commands, exp_action_args_list): @@ -507,3 +821,81 @@ def test_unknown_command(): command_factory.to_command( command_dto=CommandDTO(action="unknown_command", args={}, study_version=STUDY_VERSION_8_8) ) + + +@pytest.mark.unit_test +def test_parse_create_cluster_dto_v1(command_factory: CommandFactory): + dto = CommandDTO( + action=CommandName.CREATE_THERMAL_CLUSTER.value, + version=1, + args={ + "area_id": "area_name", + "cluster_name": "cluster_name", + "parameters": {}, + "prepro": "prepro", + "modulation": "modulation", + }, + study_version=STUDY_VERSION_8_8, + ) + commands = command_factory.to_command(dto) + assert len(commands) == 1 + command = commands[0] + dto = command.to_dto() + assert dto.version == 2 + assert dto.args["parameters"]["name"] == "cluster_name" + assert "cluster_name" not in dto.args + + +def test_parse_create_st_storage_dto_v1(command_factory: CommandFactory): + dto = CommandDTO( + action=CommandName.CREATE_ST_STORAGE.value, + version=1, + args={ + "area_id": "area_name", + "cluster_name": "cluster_name", + "parameters": { + "group": "Battery", + "injectionnominalcapacity": 0, + "withdrawalnominalcapacity": 0, + "reservoircapacity": 0, + "efficiency": 1, + "initiallevel": 0, + "initialleveloptim": False, + }, + "pmax_injection": "matrix://59ea6c83-6348-466d-9530-c35c51ca4c37", + "pmax_withdrawal": "matrix://5f988548-dadc-4bbb-8ce8-87a544dbf756", + "lower_rule_curve": "matrix://8ce4fcea-cc97-4d2c-b641-a27a53454612", + "upper_rule_curve": "matrix://8ce614c8-c687-41af-8b24-df8a49cc52af", + "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", + }, + study_version=STUDY_VERSION_8_8, + ) + commands = command_factory.to_command(dto) + assert len(commands) == 1 + command = commands[0] + dto = command.to_dto() + assert dto.version == 2 + assert dto.args["parameters"]["name"] == "cluster_name" + assert "cluster_name" not in dto.args + + +def test_parse_create_renewable_cluster_dto_v1(command_factory: CommandFactory): + dto = CommandDTO( + action=CommandName.CREATE_RENEWABLES_CLUSTER.value, + version=1, + args={ + "area_id": "area_name", + "cluster_name": "cluster_name", + "parameters": { + "ts-interpretation": "power-generation", + }, + }, + study_version=STUDY_VERSION_8_8, + ) + commands = command_factory.to_command(dto) + assert len(commands) == 1 + command = commands[0] + dto = command.to_dto() + assert dto.version == 2 + assert dto.args["parameters"]["name"] == "cluster_name" + assert "cluster_name" not in dto.args From 7f217759cdebe6a9ad0b48e0238011af5b998ddf Mon Sep 17 00:00:00 2001 From: Sylvain Leclerc Date: Wed, 12 Feb 2025 09:49:11 +0100 Subject: [PATCH 03/11] fix(st-storage): fix ST storage groups case for v8.6 (#2342) Signed-off-by: Sylvain Leclerc --- .../root/input/st_storage/clusters/area/list.py | 17 +++++++++++++++-- .../special_node/test_lower_case_nodes.py | 6 +++--- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py index 49fbdf339a..8ee1fbb53f 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/st_storage/clusters/area/list.py @@ -21,13 +21,26 @@ _VALUE_PARSERS = {any_section_option_matcher("group"): LOWER_CASE_PARSER} +_GROUPS_V8_6 = [ + "PSP_open", + "PSP_closed", + "Pondage", + "Battery", + "Other1", + "Other2", + "Other3", + "Other4", + "Other5", +] +_GROUP_MAPPING_V8_6 = {v.lower(): v for v in _GROUPS_V8_6} + def _write_group_8_6(input: str) -> str: """ The solver was not case insensitive to group, before version 8.6. - We need to write it with a capital first letter. + We need to write it respecting the expected case. """ - return input.title() + return _GROUP_MAPPING_V8_6.get(input.lower(), input.lower()) def _get_group_serializer(study_version: StudyVersion) -> ValueSerializer: diff --git a/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py b/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py index a4d70ba111..3192efc66c 100644 --- a/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py +++ b/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py @@ -151,7 +151,7 @@ def test_st_storage_group_is_written_to_title_case_for_8_6(study_dir: Path, ini_ textwrap.dedent( """ [Cluster 1] - group = Gas + group = gas """ ) ) @@ -161,5 +161,5 @@ def test_st_storage_group_is_written_to_title_case_for_8_6(study_dir: Path, ini_ area="area_test", ) - node.save({"Cluster 1": {"group": "GAS"}}) - assert IniReader().read(ini_file) == {"Cluster 1": {"group": "Gas"}} + node.save({"Cluster 1": {"group": "PsP_open"}, "Cluster 2": {"group": "UnknownGroup"}}) + assert IniReader().read(ini_file) == {"Cluster 1": {"group": "PSP_open"}, "Cluster 2": {"group": "unknowngroup"}} From a3ad8e0528d2b754244fdb6e852c0d8491b7bbf3 Mon Sep 17 00:00:00 2001 From: Hatim Dinia <33469289+hdinia@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:31:47 +0100 Subject: [PATCH 04/11] refactor(ui): update clusters group names to lowercase (#2271) --- .../Modelization/Areas/Renewables/Fields.tsx | 1 + .../Modelization/Areas/Renewables/Form.tsx | 1 - .../Modelization/Areas/Renewables/utils.ts | 18 ++++++++--------- .../Modelization/Areas/Storages/Fields.tsx | 1 + .../Modelization/Areas/Storages/Form.tsx | 1 - .../Modelization/Areas/Storages/utils.ts | 18 ++++++++--------- .../Modelization/Areas/Thermal/Fields.tsx | 1 + .../Modelization/Areas/Thermal/utils.ts | 20 +++++++++---------- 8 files changed, 31 insertions(+), 30 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Fields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Fields.tsx index a6ac2e1ed9..4d36d08c09 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Fields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Fields.tsx @@ -38,6 +38,7 @@ function Fields() { name="group" control={control} options={RENEWABLE_GROUPS} + startCaseLabel={false} sx={{ alignSelf: "center", }} diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx index a99e1a35de..a79d77467c 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/Form.tsx @@ -75,7 +75,6 @@ function Renewables() { config={{ defaultValues }} onSubmit={handleSubmit} enableUndoRedo - sx={{ height: "50%" }} > diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/utils.ts index a998b29942..daf9173aeb 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Renewables/utils.ts @@ -22,15 +22,15 @@ import type { ClusterWithCapacity } from "../common/clustersUtils"; //////////////////////////////////////////////////////////////// export const RENEWABLE_GROUPS = [ - "Wind Onshore", - "Wind Offshore", - "Solar Thermal", - "Solar PV", - "Solar Rooftop", - "Other RES 1", - "Other RES 2", - "Other RES 3", - "Other RES 4", + "wind onshore", + "wind offshore", + "solar thermal", + "solar pv", + "solar rooftop", + "other res 1", + "other res 2", + "other res 3", + "other res 4", ] as const; export const TS_INTERPRETATION_OPTIONS = ["power-generation", "production-factor"] as const; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Fields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Fields.tsx index bee66613ee..2a5cc3e7a8 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Fields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Fields.tsx @@ -44,6 +44,7 @@ function Fields() { name="group" control={control} options={STORAGE_GROUPS} + startCaseLabel={false} sx={{ alignSelf: "center", }} diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx index 8260cc1719..10f2bd9515 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/Form.tsx @@ -95,7 +95,6 @@ function Storages() { }} onSubmit={handleSubmit} enableUndoRedo - sx={{ height: "50%" }} > diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/utils.ts index 3128f0fc0e..8956746753 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Storages/utils.ts @@ -21,15 +21,15 @@ import type { PartialExceptFor } from "../../../../../../../utils/tsUtils"; //////////////////////////////////////////////////////////////// export const STORAGE_GROUPS = [ - "PSP_open", - "PSP_closed", - "Pondage", - "Battery", - "Other1", - "Other2", - "Other3", - "Other4", - "Other5", + "psp_open", + "psp_closed", + "pondage", + "battery", + "other1", + "other2", + "other3", + "other4", + "other5", ] as const; //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx index d51208842e..c6668c9d43 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/Fields.tsx @@ -53,6 +53,7 @@ function Fields() { name="group" control={control} options={THERMAL_GROUPS} + startCaseLabel={false} sx={{ alignSelf: "center", }} diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts index 28cc71d33b..1f744c2795 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Thermal/utils.ts @@ -38,16 +38,16 @@ export const TS_GEN_MATRIX_COLS = [ ] as const; export const THERMAL_GROUPS = [ - "Gas", - "Hard Coal", - "Lignite", - "Mixed fuel", - "Nuclear", - "Oil", - "Other 1", - "Other 2", - "Other 3", - "Other 4", + "gas", + "hard coal", + "lignite", + "mixed fuel", + "nuclear", + "oil", + "other 1", + "other 2", + "other 3", + "other 4", ] as const; export const THERMAL_POLLUTANTS = [ From 3965817c5c991bc50924c23f34a46d0580ba11af Mon Sep 17 00:00:00 2001 From: MartinBelthle Date: Wed, 12 Feb 2025 10:00:15 +0100 Subject: [PATCH 05/11] feat(matrix): never return empty matrix (#2296) --- antarest/matrixstore/uri_resolver_service.py | 6 +-- .../model/filesystem/common/prepro.py | 25 ++++++++-- .../rawstudy/model/filesystem/lazy_node.py | 16 +++--- .../model/filesystem/matrix/constants.py | 9 ++++ .../filesystem/matrix/input_series_matrix.py | 17 ++----- .../model/filesystem/matrix/matrix.py | 49 +++++++------------ .../input/hydro/common/capacity/capacity.py | 26 ++++++++++ .../root/input/hydro/prepro/area/area.py | 8 ++- .../filesystem/root/input/link/area/area.py | 15 +++++- .../input/link/area/capacities/capacities.py | 5 +- .../thermal/prepro/area/thermal/thermal.py | 20 +++++++- .../test_download_matrices.py | 8 +-- .../test_fetch_raw_data.py | 16 +++--- ...est_generate_thermal_cluster_timeseries.py | 2 +- tests/storage/integration/test_STA_mini.py | 2 +- .../matrix/test_input_series_matrix.py | 47 +++++++++++++++++- .../filesystem/matrix/test_matrix_node.py | 12 ++--- .../repository/filesystem/test_lazy_node.py | 20 -------- 18 files changed, 189 insertions(+), 114 deletions(-) diff --git a/antarest/matrixstore/uri_resolver_service.py b/antarest/matrixstore/uri_resolver_service.py index 542df1a49b..ae0bc4f7cd 100644 --- a/antarest/matrixstore/uri_resolver_service.py +++ b/antarest/matrixstore/uri_resolver_service.py @@ -15,7 +15,7 @@ import pandas as pd -from antarest.core.model import SUB_JSON +from antarest.core.model import JSON from antarest.matrixstore.service import ISimpleMatrixService @@ -23,7 +23,7 @@ class UriResolverService: def __init__(self, matrix_service: ISimpleMatrixService): self.matrix_service = matrix_service - def resolve(self, uri: str, formatted: bool = True) -> SUB_JSON: + def resolve(self, uri: str, formatted: bool = True) -> JSON | str | None: res = UriResolverService._extract_uri_components(uri) if res: protocol, uuid = res @@ -49,7 +49,7 @@ def extract_id(uri: str) -> Optional[str]: res = UriResolverService._extract_uri_components(uri) return res[1] if res else None - def _resolve_matrix(self, id: str, formatted: bool = True) -> SUB_JSON: + def _resolve_matrix(self, id: str, formatted: bool = True) -> JSON | str: data = self.matrix_service.get(id) if not data: raise ValueError(f"id matrix {id} not found") diff --git a/antarest/study/storage/rawstudy/model/filesystem/common/prepro.py b/antarest/study/storage/rawstudy/model/filesystem/common/prepro.py index 4cb5d533b4..a1125a9acd 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/common/prepro.py +++ b/antarest/study/storage/rawstudy/model/filesystem/common/prepro.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import numpy as np from typing_extensions import override from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig @@ -16,6 +17,7 @@ from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode from antarest.study.storage.rawstudy.model.filesystem.inode import TREE +from antarest.study.storage.rawstudy.model.filesystem.matrix.constants import default_scenario_hourly from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix @@ -44,14 +46,29 @@ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): IniFileNode.__init__(self, context, config, types={}) +default_k = np.zeros((24, 12), dtype=np.float64) +default_k.flags.writeable = False + +default_conversion = np.array([[-9999999980506447872, 0, -9999999980506447872], [0, 0, 0]], dtype=np.float64) +default_conversion.flags.writeable = False + +default_data = np.ones((12, 6), dtype=np.float64) +default_data[:, 2] = 0 +default_data.flags.writeable = False + + class PreproArea(FolderNode): @override def build(self) -> TREE: children: TREE = { - "conversion": InputSeriesMatrix(self.context, self.config.next_file("conversion.txt")), - "data": InputSeriesMatrix(self.context, self.config.next_file("data.txt")), - "k": InputSeriesMatrix(self.context, self.config.next_file("k.txt")), - "translation": InputSeriesMatrix(self.context, self.config.next_file("translation.txt")), + "conversion": InputSeriesMatrix( + self.context, self.config.next_file("conversion.txt"), default_empty=default_conversion + ), + "data": InputSeriesMatrix(self.context, self.config.next_file("data.txt"), default_empty=default_data), + "k": InputSeriesMatrix(self.context, self.config.next_file("k.txt"), default_empty=default_k), + "translation": InputSeriesMatrix( + self.context, self.config.next_file("translation.txt"), default_empty=default_scenario_hourly + ), "settings": PreproAreaSettings(self.context, self.config.next_file("settings.ini")), } return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py index ac7d19856e..ed40a73596 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/lazy_node.py @@ -51,7 +51,8 @@ def _get_real_file_path( if self.config.archive_path: path, tmp_dir = self._extract_file_to_tmp_dir(self.config.archive_path) else: - path = self.config.path + link_path = self.get_link_path() + path = link_path if link_path.exists() else self.config.path return path, tmp_dir def file_exists(self) -> bool: @@ -82,17 +83,12 @@ def _get( if get_node: return self - if self.get_link_path().exists(): - link = self.get_link_path().read_text() - if expanded: - return link - else: - return cast(G, self.context.resolver.resolve(link, formatted)) - if expanded: + if self.get_link_path().exists(): + return self.get_link_path().read_text() return self.get_lazy_content() - else: - return self.load(url, depth, expanded, formatted) + + return self.load(url, depth, expanded, formatted) @override def get( diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/constants.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/constants.py index 7c760b490c..c60a371668 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/constants.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/constants.py @@ -15,14 +15,23 @@ default_scenario_hourly = np.zeros((8760, 1), dtype=np.float64) default_scenario_hourly.flags.writeable = False +default_scenario_hourly_ones = np.ones((8760, 1), dtype=np.float64) +default_scenario_hourly_ones.flags.writeable = False + default_scenario_daily = np.zeros((365, 1), dtype=np.float64) default_scenario_daily.flags.writeable = False +default_scenario_daily_ones = np.ones((365, 1), dtype=np.float64) +default_scenario_daily_ones.flags.writeable = False + default_scenario_monthly = np.zeros((12, 1), dtype=np.float64) default_scenario_monthly.flags.writeable = False default_4_fixed_hourly = np.zeros((8760, 4), dtype=np.float64) default_4_fixed_hourly.flags.writeable = False +default_6_fixed_hourly = np.zeros((8760, 6), dtype=np.float64) +default_6_fixed_hourly.flags.writeable = False + default_8_fixed_hourly = np.zeros((8760, 8), dtype=np.float64) default_8_fixed_hourly.flags.writeable = False diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py index 5bf7c25598..9736e2dad4 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/input_series_matrix.py @@ -44,7 +44,7 @@ def __init__( config: FileStudyTreeConfig, freq: MatrixFrequency = MatrixFrequency.HOURLY, nb_columns: Optional[int] = None, - default_empty: Optional[npt.NDArray[np.float64]] = None, + default_empty: Optional[npt.NDArray[np.float64]] = None, # optional only for the capacity matrix in Xpansion ): super().__init__(context=context, config=config, freq=freq) self.nb_columns = nb_columns @@ -55,6 +55,7 @@ def __init__( self.default_empty = np.copy(default_empty) self.default_empty.flags.writeable = True + @override def parse_as_dataframe(self, file_path: Optional[Path] = None) -> pd.DataFrame: file_path = file_path or self.config.path try: @@ -82,6 +83,8 @@ def parse_as_dataframe(self, file_path: Optional[Path] = None) -> pd.DataFrame: raise ChildNotFoundError(f"File '{relpath}' not found in the study '{study_id}'") from e stopwatch.log_elapsed(lambda x: logger.info(f"Matrix parsed in {x}s")) final_matrix = matrix.dropna(how="any", axis=1) + if final_matrix.empty: + raise EmptyDataError return final_matrix except EmptyDataError: logger.warning(f"Empty file found when parsing {file_path}") @@ -90,14 +93,6 @@ def parse_as_dataframe(self, file_path: Optional[Path] = None) -> pd.DataFrame: final_matrix = pd.DataFrame(self.default_empty) return final_matrix - @override - def parse_as_json(self, file_path: Optional[Path] = None) -> JSON: - df = self.parse_as_dataframe(file_path) - stopwatch = StopWatch() - data = cast(JSON, df.to_dict(orient="split")) - stopwatch.log_elapsed(lambda x: logger.info(f"Matrix to dict in {x}s")) - return data - @override def check_errors( self, @@ -150,7 +145,3 @@ def get_file_content(self) -> OriginalFile: else: content = self.config.path.read_bytes() return OriginalFile(content=content, suffix=suffix, filename=filename) - - @override - def get_default_empty_matrix(self) -> Optional[npt.NDArray[np.float64]]: - return self.default_empty diff --git a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py index 5d720340b2..7ea1fda086 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py +++ b/antarest/study/storage/rawstudy/model/filesystem/matrix/matrix.py @@ -22,6 +22,7 @@ from typing_extensions import override from antarest.core.model import JSON +from antarest.core.utils.utils import StopWatch from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.exceptions import DenormalizationException @@ -92,13 +93,11 @@ def normalize(self) -> None: if self.get_link_path().exists() or self.config.archive_path: return - matrix = self.parse_as_json() - - if "data" in matrix: - data = cast(List[List[float]], matrix["data"]) - uuid = self.context.matrix.create(data) - self.get_link_path().write_text(self.context.resolver.build_matrix_uri(uuid)) - self.config.path.unlink() + matrix = self.parse_as_dataframe() + data = matrix.to_numpy().tolist() + uuid = self.context.matrix.create(data) + self.get_link_path().write_text(self.context.resolver.build_matrix_uri(uuid)) + self.config.path.unlink() @override def denormalize(self) -> None: @@ -128,40 +127,26 @@ def load( expanded: bool = False, formatted: bool = True, ) -> Union[bytes, JSON]: - file_path, tmp_dir = self._get_real_file_path() + file_path, _ = self._get_real_file_path() - if formatted: - return self.parse_as_json(file_path) + df = self.parse_as_dataframe(file_path) - if not file_path.exists(): - logger.warning(f"Missing file {self.config.path}") - if tmp_dir: - tmp_dir.cleanup() - return b"" - - file_content = file_path.read_bytes() - if file_content != b"": - return file_content + if formatted: + stopwatch = StopWatch() + data = cast(JSON, df.to_dict(orient="split")) + stopwatch.log_elapsed(lambda x: logger.info(f"Matrix to dict in {x}s")) + return data - # If the content is empty, we should return the default matrix to do the same as `parse_as_json()` - default_matrix = self.get_default_empty_matrix() - if default_matrix is None: + if df.empty: return b"" buffer = io.BytesIO() - np.savetxt(buffer, default_matrix, delimiter="\t") + np.savetxt(buffer, df, delimiter="\t", fmt="%.6f") return buffer.getvalue() @abstractmethod - def parse_as_json(self, file_path: Optional[Path] = None) -> JSON: - """ - Parse the matrix content and return it as a JSON object - """ - raise NotImplementedError() - - @abstractmethod - def get_default_empty_matrix(self) -> Optional[npt.NDArray[np.float64]]: + def parse_as_dataframe(self, file_path: Optional[Path] = None) -> pd.DataFrame: """ - Returns the default matrix to return when the existing one is empty + Parse the matrix content and return it as a DataFrame object """ raise NotImplementedError() diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/common/capacity/capacity.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/common/capacity/capacity.py index ca45887ac8..4d3e0a65a6 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/common/capacity/capacity.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/common/capacity/capacity.py @@ -12,12 +12,15 @@ from typing import List, TypedDict +import numpy as np +import numpy.typing as npt from antares.study.version import StudyVersion from typing_extensions import override from antarest.study.model import STUDY_VERSION_6_5 from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode from antarest.study.storage.rawstudy.model.filesystem.inode import TREE +from antarest.study.storage.rawstudy.model.filesystem.matrix.constants import default_scenario_daily_ones from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import MatrixFrequency @@ -26,8 +29,25 @@ class MatrixInfo(TypedDict, total=False): name: str freq: MatrixFrequency start_version: StudyVersion + default_empty: npt.NDArray[np.float64] +default_maxpower = np.zeros((365, 4), dtype=np.float64) +default_maxpower[:, 1] = 24 +default_maxpower[:, 3] = 24 +default_maxpower.flags.writeable = False + +default_reservoir = np.zeros((365, 3), dtype=np.float64) +default_reservoir[:, 1] = 0.5 +default_reservoir[:, 2] = 1 +default_reservoir.flags.writeable = False + +default_credit_modulation = np.ones((2, 100), dtype=np.float64) +default_credit_modulation.flags.writeable = False + +default_water_values = np.zeros((365, 101), dtype=np.float64) +default_water_values.flags.writeable = False + INITIAL_VERSION = StudyVersion.parse(0) # noinspection SpellCheckingInspection MATRICES_INFO: List[MatrixInfo] = [ @@ -35,26 +55,31 @@ class MatrixInfo(TypedDict, total=False): "name": "maxpower", "freq": MatrixFrequency.DAILY, "start_version": INITIAL_VERSION, + "default_empty": default_maxpower, }, { "name": "reservoir", "freq": MatrixFrequency.DAILY, "start_version": INITIAL_VERSION, + "default_empty": default_reservoir, }, { "name": "inflowPattern", "freq": MatrixFrequency.DAILY, "start_version": STUDY_VERSION_6_5, + "default_empty": default_scenario_daily_ones, }, { "name": "creditmodulations", "freq": MatrixFrequency.HOURLY, "start_version": STUDY_VERSION_6_5, + "default_empty": default_credit_modulation, }, { "name": "waterValues", "freq": MatrixFrequency.DAILY, "start_version": STUDY_VERSION_6_5, + "default_empty": default_water_values, }, ] @@ -71,5 +96,6 @@ def build(self) -> TREE: self.context, self.config.next_file(f"{name}.txt"), freq=info["freq"], + default_empty=info["default_empty"], ) return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/prepro/area/area.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/prepro/area/area.py index c3c47fc134..a94450d6da 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/prepro/area/area.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/hydro/prepro/area/area.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import numpy as np from typing_extensions import override from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode @@ -18,12 +19,17 @@ InputHydroPreproAreaPrepro, ) +default_energy = np.zeros((12, 5), dtype=np.float64) +default_energy.flags.writeable = False + class InputHydroPreproArea(FolderNode): @override def build(self) -> TREE: children: TREE = { - "energy": InputSeriesMatrix(self.context, self.config.next_file("energy.txt")), + "energy": InputSeriesMatrix( + self.context, self.config.next_file("energy.txt"), default_empty=default_energy + ), "prepro": InputHydroPreproAreaPrepro(self.context, self.config.next_file("prepro.ini")), } return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/area.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/area.py index f89d51cf9e..7da279607d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/area.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/area.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import numpy as np from typing_extensions import override from antarest.study.model import STUDY_VERSION_8_2 @@ -16,12 +17,17 @@ from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode from antarest.study.storage.rawstudy.model.filesystem.inode import TREE +from antarest.study.storage.rawstudy.model.filesystem.matrix.constants import default_6_fixed_hourly from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix from antarest.study.storage.rawstudy.model.filesystem.root.input.link.area.capacities.capacities import ( InputLinkAreaCapacities, ) from antarest.study.storage.rawstudy.model.filesystem.root.input.link.area.properties import InputLinkAreaProperties +default_link_legacy_matrix = np.zeros((8760, 8), dtype=np.float64) +default_link_legacy_matrix[:, :2] = 1 +default_link_legacy_matrix.flags.writeable = False + class InputLinkArea(FolderNode): def __init__( @@ -39,10 +45,15 @@ def build(self) -> TREE: ctx = self.context cfg = self.config if cfg.version < STUDY_VERSION_8_2: - children = {link: InputSeriesMatrix(ctx, cfg.next_file(f"{link}.txt")) for link in cfg.get_links(self.area)} + children = { + link: InputSeriesMatrix(ctx, cfg.next_file(f"{link}.txt"), default_empty=default_link_legacy_matrix) + for link in cfg.get_links(self.area) + } else: children = { - f"{link}_parameters": InputSeriesMatrix(ctx, cfg.next_file(f"{link}_parameters.txt")) + f"{link}_parameters": InputSeriesMatrix( + ctx, cfg.next_file(f"{link}_parameters.txt"), default_empty=default_6_fixed_hourly + ) for link in cfg.get_links(self.area) } children["capacities"] = InputLinkAreaCapacities(ctx, cfg.next_file("capacities"), area=self.area) diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/capacities/capacities.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/capacities/capacities.py index 2b908dd3ff..c8784a21e6 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/capacities/capacities.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/link/area/capacities/capacities.py @@ -15,6 +15,7 @@ from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode from antarest.study.storage.rawstudy.model.filesystem.inode import TREE +from antarest.study.storage.rawstudy.model.filesystem.matrix.constants import default_scenario_hourly_ones from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix @@ -33,12 +34,12 @@ def build(self) -> TREE: children: TREE = {} for area_to in self.config.get_links(self.area): children[f"{area_to}_direct"] = InputSeriesMatrix( - self.context, - self.config.next_file(f"{area_to}_direct.txt"), + self.context, self.config.next_file(f"{area_to}_direct.txt"), default_empty=default_scenario_hourly_ones ) children[f"{area_to}_indirect"] = InputSeriesMatrix( self.context, self.config.next_file(f"{area_to}_indirect.txt"), + default_empty=default_scenario_hourly_ones, ) return children diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/thermal/thermal.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/thermal/thermal.py index 89830b67af..ac37e3613a 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/thermal/thermal.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/thermal/thermal.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import numpy as np from typing_extensions import override from antarest.study.storage.rawstudy.model.filesystem.folder_node import FolderNode @@ -16,6 +17,14 @@ from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import MatrixFrequency +default_data_matrix = np.zeros((365, 6), dtype=np.float64) +default_data_matrix[:, :2] = 1 +default_data_matrix.flags.writeable = False + +default_modulation_matrix = np.ones((8760, 4), dtype=np.float64) +default_modulation_matrix[:, 3] = 0 +default_modulation_matrix.flags.writeable = False + class InputThermalPreproAreaThermal(FolderNode): """ @@ -30,7 +39,14 @@ class InputThermalPreproAreaThermal(FolderNode): @override def build(self) -> TREE: children: TREE = { - "data": InputSeriesMatrix(self.context, self.config.next_file("data.txt"), freq=MatrixFrequency.DAILY), - "modulation": InputSeriesMatrix(self.context, self.config.next_file("modulation.txt")), + "data": InputSeriesMatrix( + self.context, + self.config.next_file("data.txt"), + freq=MatrixFrequency.DAILY, + default_empty=default_data_matrix, + ), + "modulation": InputSeriesMatrix( + self.context, self.config.next_file("modulation.txt"), default_empty=default_modulation_matrix + ), } return children diff --git a/tests/integration/raw_studies_blueprint/test_download_matrices.py b/tests/integration/raw_studies_blueprint/test_download_matrices.py index ec097d4fcc..7198736cbf 100644 --- a/tests/integration/raw_studies_blueprint/test_download_matrices.py +++ b/tests/integration/raw_studies_blueprint/test_download_matrices.py @@ -275,7 +275,7 @@ def test_download_matrices(self, client: TestClient, user_access_token: str, int assert list(dataframe.index) == list(dataframe.columns) == ["de", "es", "fr", "it"] assert all(np.isclose(dataframe.iloc[i, i], 1.0) for i in range(len(dataframe))) - # test for empty matrix + # checks default value for an empty water_values matrix res = client.get( f"/v1/studies/{internal_study_id}/raw/download", params={"path": "input/hydro/common/capacity/waterValues_de", "format": "tsv"}, @@ -283,7 +283,7 @@ def test_download_matrices(self, client: TestClient, user_access_token: str, int assert res.status_code == 200 content = io.BytesIO(res.content) dataframe = pd.read_csv(content, index_col=0, sep="\t") - assert dataframe.empty + assert dataframe.to_numpy().tolist() == 365 * [101 * [0.0]] # modulation matrix res = client.get( @@ -328,7 +328,7 @@ def test_download_matrices(self, client: TestClient, user_access_token: str, int "('HURDLE COST', 'Euro', '')", ] - # test energy matrix to test the regex + # checks default value for an empty energy matrix res = client.get( f"/v1/studies/{internal_study_id}/raw/download", params={"path": "input/hydro/prepro/de/energy", "format": "tsv"}, @@ -336,7 +336,7 @@ def test_download_matrices(self, client: TestClient, user_access_token: str, int assert res.status_code == 200 content = io.BytesIO(res.content) dataframe = pd.read_csv(content, index_col=0, sep="\t") - assert dataframe.empty + assert dataframe.to_numpy().tolist() == 12 * [5 * [0.0]] # test the Min Gen of the 8.6 study for export_format in ["tsv", "xlsx"]: diff --git a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py index d49cd53639..b0fb30c5c8 100644 --- a/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py +++ b/tests/integration/raw_studies_blueprint/test_fetch_raw_data.py @@ -27,6 +27,9 @@ from antarest.core.tasks.model import TaskStatus from antarest.core.utils.fastapi_sqlalchemy import db from antarest.study.model import RawStudy, Study +from antarest.study.storage.rawstudy.model.filesystem.root.input.thermal.prepro.area.thermal.thermal import ( + default_data_matrix, +) from tests.integration.raw_studies_blueprint.assets import ASSETS_DIR from tests.integration.utils import wait_for @@ -229,7 +232,7 @@ def test_get_study_data(self, client: TestClient, user_access_token: str, intern written_data = res.json()["data"] if not content.decode("utf-8"): # The `GET` returns the default matrix when it's empty - expected = 8760 * [[0]] if study_type == "raw" else [[]] + expected = 8760 * [[0]] else: df = pd.read_csv(io.BytesIO(content), delimiter=delimiter, header=None).replace(",", ".", regex=True) df = df.dropna(axis=1, how="all") # We want to remove columns full of NaN at the import @@ -272,15 +275,12 @@ def test_get_study_data(self, client: TestClient, user_access_token: str, intern first_row = [float(x) for x in actual_lines[0].split("\t")] assert first_row == [100000, 100000, 0.01, 0.01, 0, 0, 0, 0] - # If ask for an empty matrix, we should have an empty binary content - res = client.get(raw_url, params={"path": "input/thermal/prepro/de/01_solar/data", "formatted": False}) - assert res.status_code == 200, res.json() - assert res.content == b"" - - # But, if we use formatted = True, we should have a JSON objet representing and empty matrix + # If ask for an empty matrix, we should return its default value res = client.get(raw_url, params={"path": "input/thermal/prepro/de/01_solar/data", "formatted": True}) assert res.status_code == 200, res.json() - assert res.json() == {"index": [], "columns": [], "data": []} + assert res.json()["index"] == list(range(365)) + assert res.json()["columns"] == list(range(6)) + assert res.json()["data"] == default_data_matrix.tolist() # We can access to the configuration the classic way, # for instance, we can get the list of areas: diff --git a/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py b/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py index c489a17ae5..c99b52de75 100644 --- a/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py +++ b/tests/integration/study_data_blueprint/test_generate_thermal_cluster_timeseries.py @@ -97,7 +97,7 @@ def test_lifecycle_nominal(self, client: TestClient, user_access_token: str) -> ) assert res.status_code == 200 data = res.json()["data"] - assert data == [[]] # no generation c.f. gen-ts parameter + assert data == 8760 * [[0]] # no generation c.f. gen-ts parameter -> empty file -> default simulator value @pytest.mark.parametrize("study_type", ["raw", "variant"]) def test_errors_and_limit_cases(self, client: TestClient, user_access_token: str, study_type: str) -> None: diff --git a/tests/storage/integration/test_STA_mini.py b/tests/storage/integration/test_STA_mini.py index bbe66a8545..deac706029 100644 --- a/tests/storage/integration/test_STA_mini.py +++ b/tests/storage/integration/test_STA_mini.py @@ -172,7 +172,7 @@ def test_sta_mini_study_antares(storage_service, url: str, expected_output: str) buffer = io.BytesIO() -np.savetxt(buffer, np.array([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] * 8760), delimiter="\t") +np.savetxt(buffer, np.array([[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]] * 8760), delimiter="\t", fmt="%.6f") expected_min_gen_response = buffer.getvalue() diff --git a/tests/storage/repository/filesystem/matrix/test_input_series_matrix.py b/tests/storage/repository/filesystem/matrix/test_input_series_matrix.py index 1312197c24..e7e80fa17d 100644 --- a/tests/storage/repository/filesystem/matrix/test_input_series_matrix.py +++ b/tests/storage/repository/filesystem/matrix/test_input_series_matrix.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import io import itertools import shutil import textwrap @@ -17,6 +17,8 @@ from pathlib import Path from unittest.mock import Mock +import numpy as np +import pandas as pd import pytest from antarest.core.exceptions import ChildNotFoundError @@ -51,7 +53,9 @@ def test_load(self, my_study_config: FileStudyTreeConfig) -> None: file.write_text(content) node = InputSeriesMatrix(context=Mock(), config=my_study_config, nb_columns=8) - actual = node.load() + + # checks formatted response + actual = node.load(formatted=True) expected = { "columns": [0, 1, 2, 3, 4, 5, 6, 7], "data": [ @@ -62,6 +66,45 @@ def test_load(self, my_study_config: FileStudyTreeConfig) -> None: } assert actual == expected + # checks binary response + # We cannot check the content as is as we're applying a transformation to the data + df_binary = pd.DataFrame(data=expected["data"]) + buffer = io.BytesIO() + np.savetxt(buffer, df_binary, delimiter="\t", fmt="%.6f") + expected_binary = buffer.getvalue() + actual_binary = node.load(formatted=False) + assert actual_binary == expected_binary + + @pytest.mark.parametrize("link", [True, False]) + def test_load_empty_file(self, my_study_config: FileStudyTreeConfig, link: bool) -> None: + file_path = my_study_config.path + default_matrix = np.array([[1, 2], [3, 4]]) + if not link: + file_path.touch() + node = InputSeriesMatrix(context=Mock(), config=my_study_config, default_empty=default_matrix) + else: + link_path = file_path.parent / f"{file_path.name}.link" + link_path.touch() + resolver = Mock() + resolver.resolve.return_value = {} + resolver.build_matrix_uri.return_value = "matrix://my-id" + matrix_service = Mock() + matrix_service.create.return_value = "my-id" + context = ContextServer(matrix=matrix_service, resolver=resolver) + node = InputSeriesMatrix(context=context, config=my_study_config, default_empty=default_matrix) + + # checks formatted response + actual = node.load(formatted=True) + expected = {"index": [0, 1], "columns": [0, 1], "data": node.default_empty.tolist()} + assert actual == expected + + # checks binary response + actual = node.load(formatted=False) + buffer = io.BytesIO() + np.savetxt(buffer, node.default_empty, delimiter="\t", fmt="%.6f") + expected = buffer.getvalue() + assert actual == expected + def test_load__file_not_found(self, my_study_config: FileStudyTreeConfig) -> None: node = InputSeriesMatrix(context=Mock(), config=my_study_config) with pytest.raises(ChildNotFoundError) as ctx: diff --git a/tests/storage/repository/filesystem/matrix/test_matrix_node.py b/tests/storage/repository/filesystem/matrix/test_matrix_node.py index 7edaa94c6e..b3607af051 100644 --- a/tests/storage/repository/filesystem/matrix/test_matrix_node.py +++ b/tests/storage/repository/filesystem/matrix/test_matrix_node.py @@ -14,11 +14,8 @@ from typing import List, Optional from unittest.mock import Mock -import numpy as np -import pandas as pd # type: ignore -from numpy import typing as npt +import pandas as pd -from antarest.core.model import JSON from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.matrix.matrix import MatrixFrequency, MatrixNode @@ -40,11 +37,8 @@ def __init__(self, context: ContextServer, config: FileStudyTreeConfig) -> None: freq=MatrixFrequency.ANNUAL, ) - def parse_as_json(self, file_path: Optional[Path] = None) -> JSON: - return MOCK_MATRIX_JSON - - def get_default_empty_matrix(self) -> Optional[npt.NDArray[np.float64]]: - pass + def parse_as_dataframe(self, file_path: Optional[Path] = None) -> pd.DataFrame: + return pd.DataFrame(MOCK_MATRIX_DTO) def check_errors(self, data: str, url: Optional[List[str]] = None, raising: bool = False) -> List[str]: pass # not used diff --git a/tests/storage/repository/filesystem/test_lazy_node.py b/tests/storage/repository/filesystem/test_lazy_node.py index 8564a80e1b..d1f231d408 100644 --- a/tests/storage/repository/filesystem/test_lazy_node.py +++ b/tests/storage/repository/filesystem/test_lazy_node.py @@ -62,26 +62,6 @@ def test_get_no_expanded_txt(tmp_path: Path): assert "Mock Matrix Content" == node.get(expanded=False) -def test_get_no_expanded_link(tmp_path: Path): - uri = "matrix://my-link" - - file = tmp_path / "my-study/lazy.txt" - file.parent.mkdir() - (file.parent / "lazy.txt.link").write_text(uri) - - config = FileStudyTreeConfig(study_path=file, path=file, version=-1, study_id="my-study") - - resolver = Mock() - resolver.resolve.return_value = "Mock Matrix Content" - - node = MockLazyNode( - context=ContextServer(matrix=Mock(), resolver=resolver), - config=config, - ) - assert "Mock Matrix Content" == node.get(expanded=False) - resolver.resolve.assert_called_once_with(uri, True) - - def test_get_expanded_txt(tmp_path: Path): file = tmp_path / "my-study/lazy.txt" file.parent.mkdir() From d7f1e358d265c7400ca8c74aa83b59eb62239842 Mon Sep 17 00:00:00 2001 From: Theo Pascoli <48944759+TheoPascoli@users.noreply.github.com> Date: Thu, 13 Feb 2025 15:40:35 +0100 Subject: [PATCH 06/11] refactor(all): remove use of pydantic v1 deprecated methods (#2344) --- antarest/eventbus/business/redis_eventbus.py | 4 ++- antarest/eventbus/web.py | 3 ++- antarest/matrixstore/model.py | 4 +-- antarest/matrixstore/repository.py | 6 ++--- antarest/matrixstore/service.py | 4 +-- .../study/business/allocation_management.py | 9 ++++--- antarest/study/business/area_management.py | 2 +- .../study/business/areas/hydro_management.py | 4 ++- .../business/areas/renewable_management.py | 13 ++++------ .../business/areas/st_storage_management.py | 12 ++++----- .../business/areas/thermal_management.py | 14 +++++----- .../business/binding_constraint_management.py | 2 +- .../study/business/correlation_management.py | 16 +++++++----- antarest/study/business/general_management.py | 2 +- .../study/business/optimization_management.py | 2 +- .../study/business/playlist_management.py | 2 +- .../study/business/xpansion_management.py | 4 +-- antarest/study/model.py | 2 +- antarest/study/storage/patch_service.py | 8 +++--- .../rawstudy/model/filesystem/config/model.py | 2 +- .../study/storage/study_download_utils.py | 2 +- antarest/study/storage/utils.py | 2 +- tests/conftest.py | 2 -- .../filesystem_blueprint/test_model.py | 1 - .../test_hydro_allocation.py | 2 +- .../test_hydro_correlation.py | 2 +- .../study_data_blueprint/test_st_storage.py | 1 - tests/launcher/test_slurm_launcher.py | 2 +- tests/login/test_web.py | 4 +-- .../storage/business/test_arealink_manager.py | 2 +- tests/storage/conftest.py | 2 +- tests/storage/test_service.py | 18 ++++++------- tests/storage/web/test_studies_bp.py | 2 +- .../study/business/test_allocation_manager.py | 26 +++++++++---------- .../rawstudy/test_raw_study_service.py | 1 - .../test_variant_study_service.py | 1 - tests/study/test_field_validators.py | 4 +-- .../model/command/test_create_st_storage.py | 1 - .../model/command/test_remove_cluster.py | 2 -- .../model/command/test_replace_matrix.py | 5 ---- .../model/command/test_update_comments.py | 2 -- .../variantstudy/model/test_variant_model.py | 1 - tests/variantstudy/test_command_factory.py | 2 +- 43 files changed, 93 insertions(+), 109 deletions(-) diff --git a/antarest/eventbus/business/redis_eventbus.py b/antarest/eventbus/business/redis_eventbus.py index 161a15cc82..2e4f5c745b 100644 --- a/antarest/eventbus/business/redis_eventbus.py +++ b/antarest/eventbus/business/redis_eventbus.py @@ -11,6 +11,7 @@ # This file is part of the Antares project. import logging +import pathlib from typing import List, Optional, cast from redis.client import Redis @@ -42,7 +43,8 @@ def queue_event(self, event: Event, queue: str) -> None: def pull_queue(self, queue: str) -> Optional[Event]: event = self.redis.lpop(queue) if event: - return cast(Optional[Event], Event.parse_raw(event)) + event_string = pathlib.Path(event).read_text() + return cast(Optional[Event], Event.model_validate_json(event_string)) return None @override diff --git a/antarest/eventbus/web.py b/antarest/eventbus/web.py index ffcee5236a..3decb495cd 100644 --- a/antarest/eventbus/web.py +++ b/antarest/eventbus/web.py @@ -12,6 +12,7 @@ import dataclasses import logging +import pathlib from enum import StrEnum from http import HTTPStatus from typing import List, Optional @@ -75,7 +76,7 @@ def process_message(self, message: str, websocket: WebSocket) -> None: if not connection: return - ws_message = WebsocketMessage.parse_raw(message) + ws_message = WebsocketMessage.model_validate_json(message) if ws_message.action == WebsocketMessageAction.SUBSCRIBE: if ws_message.payload not in connection.channel_subscriptions: connection.channel_subscriptions.append(ws_message.payload) diff --git a/antarest/matrixstore/model.py b/antarest/matrixstore/model.py index c5c4222ed4..eb4c8400e8 100644 --- a/antarest/matrixstore/model.py +++ b/antarest/matrixstore/model.py @@ -219,8 +219,8 @@ def __eq__(self, other: Any) -> bool: class MatrixDTO(AntaresBaseModel): width: int height: int - index: List[str] - columns: List[str] + index: List[int | str] + columns: List[int | str] data: List[List[MatrixData]] created_at: int = 0 id: str = "" diff --git a/antarest/matrixstore/repository.py b/antarest/matrixstore/repository.py index c3ac28232a..0fb766d66c 100644 --- a/antarest/matrixstore/repository.py +++ b/antarest/matrixstore/repository.py @@ -173,9 +173,9 @@ def get(self, matrix_hash: str) -> MatrixContent: matrix = storage_format.load_matrix(matrix_path) matrix = matrix.reshape((1, 0)) if matrix.size == 0 else matrix data = matrix.tolist() - index = list(range(matrix.shape[0])) - columns = list(range(matrix.shape[1])) - return MatrixContent.construct(data=data, columns=columns, index=index) + index: List[int | str] = list(range(matrix.shape[0])) + columns: List[int | str] = list(range(matrix.shape[1])) + return MatrixContent.model_construct(data=data, columns=columns, index=index) def exists(self, matrix_hash: str) -> bool: """ diff --git a/antarest/matrixstore/service.py b/antarest/matrixstore/service.py index 6db331ed9c..47cab79205 100644 --- a/antarest/matrixstore/service.py +++ b/antarest/matrixstore/service.py @@ -120,7 +120,7 @@ def create(self, data: List[List[MatrixData]] | npt.NDArray[np.float64]) -> str: @override def get(self, matrix_id: str) -> MatrixDTO: data = self.matrix_content_repository.get(matrix_id) - return MatrixDTO.construct( + return MatrixDTO.model_construct( id=matrix_id, width=len(data.columns), height=len(data.index), @@ -394,7 +394,7 @@ def get(self, matrix_id: str) -> Optional[MatrixDTO]: if matrix is None: return None content = self.matrix_content_repository.get(matrix_id) - return MatrixDTO.construct( + return MatrixDTO.model_construct( id=matrix.id, width=matrix.width, height=matrix.height, diff --git a/antarest/study/business/allocation_management.py b/antarest/study/business/allocation_management.py index 0b8577a854..1d2950b0e9 100644 --- a/antarest/study/business/allocation_management.py +++ b/antarest/study/business/allocation_management.py @@ -117,7 +117,7 @@ class AllocationManager: def __init__(self, storage_service: StudyStorageService) -> None: self.storage_service = storage_service - def get_allocation_data(self, study: Study, area_id: str) -> Dict[str, List[AllocationField]]: + def get_allocation_data(self, study: Study, area_id: str) -> Dict[str, float]: """ Get hydraulic allocation data. @@ -160,11 +160,12 @@ def get_allocation_form_fields( """ areas_ids = {area.id for area in all_areas} - allocations = self.get_allocation_data(study, area_id) + allocations: Dict[str, float] = self.get_allocation_data(study, area_id) filtered_allocations = {area: value for area, value in allocations.items() if area in areas_ids} final_allocations = [ - AllocationField.construct(area_id=area, coefficient=value) for area, value in filtered_allocations.items() + AllocationField.model_construct(area_id=area, coefficient=value) + for area, value in filtered_allocations.items() ] return AllocationFormFields.model_validate({"allocation": final_allocations}) @@ -212,7 +213,7 @@ def set_allocation_form_fields( return AllocationFormFields.model_construct( allocation=[ - AllocationField.construct(area_id=area, coefficient=value) + AllocationField.model_construct(area_id=area, coefficient=value) for area, value in updated_allocations.items() ] ) diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 15cd7e61d7..9d83e9be7a 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -189,7 +189,7 @@ def update_areas_props( for area_id, update_area in update_areas_by_ids.items(): # Update the area properties. old_area = old_areas_by_ids[area_id] - new_area = old_area.copy(update=update_area.model_dump(mode="json", by_alias=False, exclude_none=True)) + new_area = old_area.model_copy(update=update_area.model_dump(mode="json", exclude_none=True)) new_areas_by_ids[area_id] = new_area # Convert the DTO to a configuration object and update the configuration file. diff --git a/antarest/study/business/areas/hydro_management.py b/antarest/study/business/areas/hydro_management.py index 436947af3f..6a43eaeaad 100644 --- a/antarest/study/business/areas/hydro_management.py +++ b/antarest/study/business/areas/hydro_management.py @@ -123,7 +123,9 @@ def get_value(field_info: FieldInfo) -> Any: target_name = path.split("/")[-1] return hydro_config.get(target_name, {}).get(area_id, field_info["default_value"]) - return ManagementOptionsFormFields.construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) + return ManagementOptionsFormFields.model_construct( + **{name: get_value(info) for name, info in FIELDS_INFO.items()} + ) def set_field_values( self, diff --git a/antarest/study/business/areas/renewable_management.py b/antarest/study/business/areas/renewable_management.py index 81a41147f6..15346f0820 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -277,9 +277,8 @@ def update_cluster( else: old_config = create_renewable_config(study_version, **values) - # use Python values to synchronize Config and Form values - new_values = cluster_data.model_dump(by_alias=False, exclude_none=True) - new_config = old_config.copy(exclude={"id"}, update=new_values) + new_values = cluster_data.model_dump(exclude_none=True) + new_config = old_config.model_copy(update=new_values) new_data = new_config.model_dump(mode="json", by_alias=True, exclude={"id"}) # create the dict containing the new values using aliases @@ -299,7 +298,7 @@ def update_cluster( ] execute_or_add_commands(study, file_study, commands, self.storage_service) - values = new_config.model_dump(by_alias=False) + values = new_config.model_dump(exclude={"id"}) return RenewableClusterOutput(**values, id=cluster_id) def delete_clusters(self, study: Study, area_id: str, cluster_ids: Sequence[str]) -> None: @@ -395,13 +394,11 @@ def update_renewables_props( for renewable_id, update_cluster in update_renewables_by_ids.items(): # Update the renewable cluster properties. old_cluster = old_renewables_by_ids[renewable_id] - new_cluster = old_cluster.copy(update=update_cluster.model_dump(by_alias=False, exclude_none=True)) + new_cluster = old_cluster.model_copy(update=update_cluster.model_dump(exclude_none=True)) new_renewables_by_areas[area_id][renewable_id] = new_cluster # Convert the DTO to a configuration object and update the configuration file. - properties = create_renewable_config( - study_version, **new_cluster.model_dump(by_alias=False, exclude_none=True) - ) + properties = create_renewable_config(study_version, **new_cluster.model_dump(exclude_none=True)) path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=renewable_id) cmd = UpdateConfig( target=path, diff --git a/antarest/study/business/areas/st_storage_management.py b/antarest/study/business/areas/st_storage_management.py index 681649970c..fa94ea046b 100644 --- a/antarest/study/business/areas/st_storage_management.py +++ b/antarest/study/business/areas/st_storage_management.py @@ -397,15 +397,13 @@ def update_storages_props( for storage_id, update_cluster in update_storages_by_ids.items(): # Update the storage cluster properties. old_cluster = old_storages_by_ids[storage_id] - new_cluster = old_cluster.copy( - update=update_cluster.model_dump(mode="json", by_alias=False, exclude_none=True) - ) + new_cluster = old_cluster.model_copy(update=update_cluster.model_dump(mode="json", exclude_none=True)) new_storages_by_areas[area_id][storage_id] = new_cluster # Convert the DTO to a configuration object and update the configuration file. properties = create_st_storage_config( study_version, - **new_cluster.model_dump(mode="json", by_alias=False, exclude_none=True), + **new_cluster.model_dump(mode="json", exclude_none=True), ) path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) cmd = UpdateConfig( @@ -480,8 +478,8 @@ def update_storage( old_config = create_st_storage_config(study_version, **values) # use Python values to synchronize Config and Form values - new_values = form.model_dump(mode="json", by_alias=False, exclude_none=True) - new_config = old_config.copy(exclude={"id"}, update=new_values) + new_values = form.model_dump(mode="json", exclude_none=True) + new_config = old_config.model_copy(update=new_values) new_data = new_config.model_dump(mode="json", by_alias=True, exclude={"id"}) # create the dict containing the new values using aliases @@ -502,7 +500,7 @@ def update_storage( ] execute_or_add_commands(study, file_study, commands, self.storage_service) - values = new_config.model_dump(mode="json", by_alias=False) + values = new_config.model_dump(mode="json", exclude={"id"}) return STStorageOutput(**values, id=storage_id) def delete_storages( diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 11531eb9fd..34849324d5 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -258,15 +258,13 @@ def update_thermals_props( for thermal_id, update_cluster in update_thermals_by_ids.items(): # Update the thermal cluster properties. old_cluster = old_thermals_by_ids[thermal_id] - new_cluster = old_cluster.copy( - update=update_cluster.model_dump(mode="json", by_alias=False, exclude_none=True) - ) + new_cluster = old_cluster.model_copy(update=update_cluster.model_dump(mode="json", exclude_none=True)) new_thermals_by_areas[area_id][thermal_id] = new_cluster # Convert the DTO to a configuration object and update the configuration file. properties = create_thermal_config( study_version, - **new_cluster.model_dump(mode="json", by_alias=False, exclude_none=True), + **new_cluster.model_dump(mode="json", exclude_none=True), ) path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=thermal_id) cmd = UpdateConfig( @@ -359,8 +357,8 @@ def update_cluster( old_config = create_thermal_config(study_version, **values) # Use Python values to synchronize Config and Form values - new_values = cluster_data.model_dump(mode="json", by_alias=False, exclude_none=True) - new_config = old_config.copy(exclude={"id"}, update=new_values) + new_values = cluster_data.model_dump(mode="json", exclude_none=True) + new_config = old_config.model_copy(update=new_values) new_data = new_config.model_dump(mode="json", by_alias=True, exclude={"id"}) # create the dict containing the new values using aliases @@ -380,8 +378,8 @@ def update_cluster( ] execute_or_add_commands(study, file_study, commands, self.storage_service) - values = {**new_config.model_dump(mode="json", by_alias=False), "id": cluster_id} - return ThermalClusterOutput.model_validate(values) + values = {**new_config.model_dump(mode="json", exclude={"id"})} + return ThermalClusterOutput(**values, id=cluster_id) def delete_clusters(self, study: Study, area_id: str, cluster_ids: Sequence[str]) -> None: """ diff --git a/antarest/study/business/binding_constraint_management.py b/antarest/study/business/binding_constraint_management.py index 08cde618d6..e4471b3e75 100644 --- a/antarest/study/business/binding_constraint_management.py +++ b/antarest/study/business/binding_constraint_management.py @@ -982,7 +982,7 @@ def update_binding_constraints( if bc_id not in dict_config: raise BindingConstraintNotFound(f"Binding constraint '{bc_id}' not found") - props = create_binding_constraint_config(study_version, **value.dict()) + props = create_binding_constraint_config(study_version, **value.model_dump()) new_values = props.model_dump(mode="json", by_alias=True, exclude_unset=True) upd_obj = config[dict_config[bc_id]] current_value = copy.deepcopy(upd_obj) diff --git a/antarest/study/business/correlation_management.py b/antarest/study/business/correlation_management.py index 76a940fa70..cc8ac58853 100644 --- a/antarest/study/business/correlation_management.py +++ b/antarest/study/business/correlation_management.py @@ -249,7 +249,7 @@ def get_correlation_form_fields( column = array[:, area_ids.index(area_id)] * 100 correlation_field = [ - AreaCoefficientItem.construct(area_id=a, coefficient=c) + AreaCoefficientItem.model_construct(area_id=a, coefficient=c) for a, c in zip(area_ids, column) if a != area_id and c ] @@ -257,10 +257,10 @@ def get_correlation_form_fields( current_area_coefficient = column[area_ids.index(area_id)] correlation_field.insert( 0, - AreaCoefficientItem.construct(area_id=area_id, coefficient=current_area_coefficient), + AreaCoefficientItem.model_construct(area_id=area_id, coefficient=current_area_coefficient), ) - return CorrelationFormFields.construct(correlation=correlation_field) + return CorrelationFormFields.model_construct(correlation=correlation_field) def set_correlation_form_fields( self, @@ -301,8 +301,10 @@ def set_correlation_form_fields( self._set_array(study, file_study, area_ids, array) column = array[:, area_ids.index(area_id)] * 100 - return CorrelationFormFields.construct( - correlation=[AreaCoefficientItem.construct(area_id=a, coefficient=c) for a, c in zip(area_ids, column) if c] + return CorrelationFormFields.model_construct( + correlation=[ + AreaCoefficientItem.model_construct(area_id=a, coefficient=c) for a, c in zip(area_ids, column) if c + ] ) def get_correlation_matrix( @@ -326,7 +328,7 @@ def get_correlation_matrix( # noinspection PyTypeChecker data = [[c for i, c in enumerate(row) if area_ids[i] in columns] for row in array.tolist()] - return CorrelationMatrix.construct(index=area_ids, columns=columns, data=data) + return CorrelationMatrix.model_construct(index=area_ids, columns=columns, data=data) def set_correlation_matrix( self, @@ -364,4 +366,4 @@ def set_correlation_matrix( # noinspection PyTypeChecker data = [[c for i, c in enumerate(row) if area_ids[i] in matrix.columns] for row in array.tolist()] - return CorrelationMatrix.construct(index=area_ids, columns=matrix.columns, data=data) + return CorrelationMatrix.model_construct(index=area_ids, columns=matrix.columns, data=data) diff --git a/antarest/study/business/general_management.py b/antarest/study/business/general_management.py index e8e74822c1..0fbca596e7 100644 --- a/antarest/study/business/general_management.py +++ b/antarest/study/business/general_management.py @@ -250,7 +250,7 @@ def get_value(field_name: str, field_info: FieldInfo) -> Any: return parent.get(target_name, field_info["default_value"]) if is_in_version else None - return GeneralFormFields.construct(**{name: get_value(name, info) for name, info in FIELDS_INFO.items()}) + return GeneralFormFields.model_construct(**{name: get_value(name, info) for name, info in FIELDS_INFO.items()}) def set_field_values(self, study: Study, field_values: GeneralFormFields) -> None: """ diff --git a/antarest/study/business/optimization_management.py b/antarest/study/business/optimization_management.py index e6cba79056..1912f2b372 100644 --- a/antarest/study/business/optimization_management.py +++ b/antarest/study/business/optimization_management.py @@ -138,7 +138,7 @@ def get_value(field_info: FieldInfo) -> Any: return parent.get(target_name, field_info["default_value"]) if is_in_version else None - return OptimizationFormFields.construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) + return OptimizationFormFields.model_construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) def set_field_values(self, study: Study, field_values: OptimizationFormFields) -> None: """ diff --git a/antarest/study/business/playlist_management.py b/antarest/study/business/playlist_management.py index 4b3de8c010..30a93d56c2 100644 --- a/antarest/study/business/playlist_management.py +++ b/antarest/study/business/playlist_management.py @@ -42,7 +42,7 @@ def get_table_data( nb_years = file_study.tree.get(FIELDS_INFO["nb_years"]["path"].split("/")) or len(playlist) return { - year: PlaylistColumns.construct( + year: PlaylistColumns.model_construct( status=year in playlist, # TODO the real value for disable year weight=playlist.get(year, DEFAULT_WEIGHT), diff --git a/antarest/study/business/xpansion_management.py b/antarest/study/business/xpansion_management.py index f855e45ec7..70e9194676 100644 --- a/antarest/study/business/xpansion_management.py +++ b/antarest/study/business/xpansion_management.py @@ -376,9 +376,9 @@ def update_xpansion_settings( actual_settings = self.get_xpansion_settings(study) settings_fields = new_xpansion_settings.model_dump( - mode="json", by_alias=False, exclude_none=True, exclude={"sensitivity_config"} + mode="json", exclude_none=True, exclude={"sensitivity_config"} ) - updated_settings = actual_settings.copy(deep=True, update=settings_fields) + updated_settings = actual_settings.model_copy(deep=True, update=settings_fields) file_study = self.study_storage_service.get_storage(study).get_raw(study) diff --git a/antarest/study/model.py b/antarest/study/model.py index a07be97d1a..df0361c2f4 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -622,7 +622,7 @@ class MatrixAggregationResult(AntaresBaseModel): warnings: List[str] def to_dto(self) -> MatrixAggregationResultDTO: - return MatrixAggregationResultDTO.construct( + return MatrixAggregationResultDTO.model_construct( index=self.index, data=[ TimeSeriesData.model_construct( diff --git a/antarest/study/storage/patch_service.py b/antarest/study/storage/patch_service.py index b469a23765..c1c5bf297f 100644 --- a/antarest/study/storage/patch_service.py +++ b/antarest/study/storage/patch_service.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import pathlib from pathlib import Path from typing import Optional @@ -39,7 +39,8 @@ def get(self, study: RawStudy | VariantStudy, get_from_file: bool = False) -> Pa patch = Patch() patch_path = Path(study.path) / PATCH_JSON if patch_path.exists(): - patch = Patch.parse_file(patch_path) + json_string = pathlib.Path(patch_path).read_text() + patch = Patch.model_validate_json(json_string) return patch @@ -47,7 +48,8 @@ def get_from_filestudy(self, file_study: FileStudy) -> Patch: patch = Patch() patch_path = (Path(file_study.config.study_path)) / PATCH_JSON if patch_path.exists(): - patch = Patch.parse_file(patch_path) + json_string = pathlib.Path(patch_path).read_text() + patch = Patch.model_validate_json(json_string) return patch def set_reference_output( diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/model.py b/antarest/study/storage/rawstudy/model/filesystem/config/model.py index 71e3f01837..8fa858a7f0 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/model.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/model.py @@ -306,7 +306,7 @@ class FileStudyTreeConfigDTO(AntaresBaseModel): def from_build_config( config: FileStudyTreeConfig, ) -> "FileStudyTreeConfigDTO": - return FileStudyTreeConfigDTO.construct( + return FileStudyTreeConfigDTO.model_construct( study_path=config.study_path, path=config.path, study_id=config.study_id, diff --git a/antarest/study/storage/study_download_utils.py b/antarest/study/storage/study_download_utils.py index bd49016fdc..f772c2eb6f 100644 --- a/antarest/study/storage/study_download_utils.py +++ b/antarest/study/storage/study_download_utils.py @@ -91,7 +91,7 @@ def read_columns( matrix.data[target][year_str] = [] matrix.data[target][year_str].append( - TimeSerie.construct( + TimeSerie.model_construct( name=column_name, unit=column[1] if len(column) > 1 else "", data=[row[index] for row in rows], diff --git a/antarest/study/storage/utils.py b/antarest/study/storage/utils.py index 4cdd81ccb8..964473b7cb 100644 --- a/antarest/study/storage/utils.py +++ b/antarest/study/storage/utils.py @@ -362,7 +362,7 @@ def _get_steps( break first_week_size = first_week_offset if first_week_offset != 0 else 7 - return MatrixIndex.construct( + return MatrixIndex.model_construct( start_date=str(start_date), steps=steps, first_week_size=first_week_size, diff --git a/tests/conftest.py b/tests/conftest.py index b6e83bbac2..dd39f681d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,8 +13,6 @@ from pathlib import Path from typing import Callable -import pytest - # noinspection PyUnresolvedReferences from tests.conftest_db import * diff --git a/tests/integration/filesystem_blueprint/test_model.py b/tests/integration/filesystem_blueprint/test_model.py index 4e04761bee..a051dbd1f9 100644 --- a/tests/integration/filesystem_blueprint/test_model.py +++ b/tests/integration/filesystem_blueprint/test_model.py @@ -13,7 +13,6 @@ import asyncio import datetime import re -import shutil from pathlib import Path from pytest_mock import MockerFixture diff --git a/tests/integration/study_data_blueprint/test_hydro_allocation.py b/tests/integration/study_data_blueprint/test_hydro_allocation.py index d7e856f2c6..45e82f7ee9 100644 --- a/tests/integration/study_data_blueprint/test_hydro_allocation.py +++ b/tests/integration/study_data_blueprint/test_hydro_allocation.py @@ -191,7 +191,7 @@ def test_create_area(self, client: TestClient, user_access_token: str, internal_ res = client.post( f"/v1/studies/{internal_study_id}/areas", headers={"Authorization": f"Bearer {user_access_token}"}, - data=area_info.json(), + data=area_info.model_dump_json(), ) assert res.status_code == http.HTTPStatus.OK, res.json() diff --git a/tests/integration/study_data_blueprint/test_hydro_correlation.py b/tests/integration/study_data_blueprint/test_hydro_correlation.py index 42a5338e0f..1496149910 100644 --- a/tests/integration/study_data_blueprint/test_hydro_correlation.py +++ b/tests/integration/study_data_blueprint/test_hydro_correlation.py @@ -241,7 +241,7 @@ def test_create_area(self, client: TestClient, user_access_token: str, internal_ res = client.post( f"/v1/studies/{internal_study_id}/areas", headers={"Authorization": f"Bearer {user_access_token}"}, - data=area_info.json(), + data=area_info.model_dump_json(), ) assert res.status_code == HTTPStatus.OK, res.json() diff --git a/tests/integration/study_data_blueprint/test_st_storage.py b/tests/integration/study_data_blueprint/test_st_storage.py index 32b90b926b..8ece8ee96d 100644 --- a/tests/integration/study_data_blueprint/test_st_storage.py +++ b/tests/integration/study_data_blueprint/test_st_storage.py @@ -10,7 +10,6 @@ # # This file is part of the Antares project. -import json import re import typing as t from unittest.mock import ANY diff --git a/tests/launcher/test_slurm_launcher.py b/tests/launcher/test_slurm_launcher.py index 36f71bc51a..e6640e16cf 100644 --- a/tests/launcher/test_slurm_launcher.py +++ b/tests/launcher/test_slurm_launcher.py @@ -214,7 +214,7 @@ def test_extra_parameters(launcher_config: Config) -> None: assert launcher_params.n_cpu == slurm_config.nb_cores.default # out of range _config_time_limit = launcher_config.launcher.slurm.time_limit - launcher_params = apply_params(LauncherParametersDTO.construct(time_limit=None)) + launcher_params = apply_params(LauncherParametersDTO.model_construct(time_limit=None)) assert launcher_params.time_limit == _config_time_limit.default * 3600 launcher_params = apply_params(LauncherParametersDTO(time_limit=10)) # 10 seconds diff --git a/tests/login/test_web.py b/tests/login/test_web.py index ec896f5628..467707721c 100644 --- a/tests/login/test_web.py +++ b/tests/login/test_web.py @@ -21,7 +21,7 @@ from fastapi import FastAPI from starlette.testclient import TestClient -from antarest.core.application import AppBuildContext, create_app_ctxt +from antarest.core.application import create_app_ctxt from antarest.core.config import Config, SecurityConfig from antarest.core.jwt import JWTGroup, JWTUser from antarest.core.requests import RequestParameters @@ -95,7 +95,7 @@ def create_auth_token( impersonator=0, type="users", groups=[JWTGroup(id="group", name="group", role=RoleType.ADMIN)], - ).json(), + ).model_dump_json(), ) return {"Authorization": f"Bearer {token if isinstance(token, str) else token.decode()}"} diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index 2763827a0c..e71a9113d4 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -13,7 +13,7 @@ import json import uuid from pathlib import Path -from unittest.mock import Mock, patch +from unittest.mock import Mock from zipfile import ZipFile import pytest diff --git a/tests/storage/conftest.py b/tests/storage/conftest.py index 59654ed35a..df2d44816d 100644 --- a/tests/storage/conftest.py +++ b/tests/storage/conftest.py @@ -13,7 +13,7 @@ import datetime import uuid from pathlib import Path -from typing import Callable, Dict, List, Optional, Union +from typing import Dict, List, Optional, Union from unittest.mock import Mock import pytest diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index 06a7d45b0f..58af2370b6 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -624,7 +624,7 @@ def test_download_output() -> None: params=RequestParameters(JWTUser(id=0, impersonator=0, type="users")), ), ) - assert MatrixAggregationResultDTO.parse_raw(res.body) == res_matrix + assert MatrixAggregationResultDTO.model_validate_json(res.body) == res_matrix # AREA TYPE - ZIP & TASK export_file_download = FileDownload( @@ -684,7 +684,7 @@ def test_download_output() -> None: params=RequestParameters(JWTUser(id=0, impersonator=0, type="users")), ), ) - assert MatrixAggregationResultDTO.parse_raw(res.body) == res_matrix + assert MatrixAggregationResultDTO.model_validate_json(res.body) == res_matrix # CLUSTER TYPE input_data.type = StudyDownloadType.DISTRICT @@ -722,7 +722,7 @@ def test_download_output() -> None: params=RequestParameters(JWTUser(id=0, impersonator=0, type="users")), ), ) - assert MatrixAggregationResultDTO.parse_raw(res.body) == res_matrix + assert MatrixAggregationResultDTO.model_validate_json(res.body) == res_matrix # noinspection PyArgumentList @@ -879,22 +879,22 @@ def test_check_errors() -> None: @pytest.mark.unit_test def test_study_match() -> None: assert not study_matcher(name=None, folder="ab", workspace="hell")( - StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + StudyMetadataDTO.model_construct(id="1", folder="abc/de", workspace="hello") ) assert study_matcher(name=None, folder="ab", workspace="hello")( - StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + StudyMetadataDTO.model_construct(id="1", folder="abc/de", workspace="hello") ) assert not study_matcher(name=None, folder="abd", workspace="hello")( - StudyMetadataDTO.construct(id="1", folder="abc/de", workspace="hello") + StudyMetadataDTO.model_construct(id="1", folder="abc/de", workspace="hello") ) assert not study_matcher(name=None, folder="ab", workspace="hello")( - StudyMetadataDTO.construct(id="1", workspace="hello") + StudyMetadataDTO.model_construct(id="1", workspace="hello") ) assert study_matcher(name="f", folder=None, workspace="hello")( - StudyMetadataDTO.construct(id="1", name="foo", folder="abc/de", workspace="hello") + StudyMetadataDTO.model_construct(id="1", name="foo", folder="abc/de", workspace="hello") ) assert not study_matcher(name="foob", folder=None, workspace="hell")( - StudyMetadataDTO.construct(id="1", name="foo", folder="abc/de", workspace="hello") + StudyMetadataDTO.model_construct(id="1", name="foo", folder="abc/de", workspace="hello") ) diff --git a/tests/storage/web/test_studies_bp.py b/tests/storage/web/test_studies_bp.py index cd3d8364b3..bfbb647a7d 100644 --- a/tests/storage/web/test_studies_bp.py +++ b/tests/storage/web/test_studies_bp.py @@ -480,7 +480,7 @@ def test_sim_result() -> None: client = create_test_client(mock_service, raise_server_exceptions=False) res = client.get(f"/v1/studies/{study_id}/outputs") - actual_object = [StudySimResultDTO.parse_obj(res.json()[0])] + actual_object = [StudySimResultDTO.model_validate(res.json()[0])] assert actual_object == result_data diff --git a/tests/study/business/test_allocation_manager.py b/tests/study/business/test_allocation_manager.py index 9b9e7538fd..855e2382de 100644 --- a/tests/study/business/test_allocation_manager.py +++ b/tests/study/business/test_allocation_manager.py @@ -312,7 +312,7 @@ def test_get_allocation_form_fields__nominal_case(self, db_session, study_storag fields = manager.get_allocation_form_fields(all_areas=all_areas, study=study, area_id=area_id) expected_allocation = [ - AllocationField.construct(area_id=area, coefficient=value) + AllocationField.model_construct(area_id=area, coefficient=value) for area, value in allocation_cfg[area_id]["[allocation]"].items() ] assert fields.allocation == expected_allocation @@ -358,11 +358,11 @@ def test_set_allocation_form_fields__nominal_case(self, db_session, study_storag all_areas=all_areas, study=study, area_id=area_id, - data=AllocationFormFields.construct( + data=AllocationFormFields.model_construct( allocation=[ - AllocationField.construct(area_id="e", coefficient=0.5), - AllocationField.construct(area_id="s", coefficient=0.25), - AllocationField.construct(area_id="w", coefficient=0.25), + AllocationField.model_construct(area_id="e", coefficient=0.5), + AllocationField.model_construct(area_id="s", coefficient=0.25), + AllocationField.model_construct(area_id="w", coefficient=0.25), ], ), ) @@ -401,11 +401,11 @@ def test_set_allocation_form_fields__no_allocation_data(self, db_session, study_ all_areas=all_areas, study=study, area_id=area_id, - data=AllocationFormFields.construct( + data=AllocationFormFields.model_construct( allocation=[ - AllocationField.construct(area_id="e", coefficient=0.5), - AllocationField.construct(area_id="s", coefficient=0.25), - AllocationField.construct(area_id="w", coefficient=0.25), + AllocationField.model_construct(area_id="e", coefficient=0.5), + AllocationField.model_construct(area_id="s", coefficient=0.25), + AllocationField.model_construct(area_id="w", coefficient=0.25), ], ), ) @@ -424,11 +424,11 @@ def test_set_allocation_form_fields__invalid_area_ids(self, db_session, study_st area_id = "n" manager = AllocationManager(study_storage_service) - data = AllocationFormFields.construct( + data = AllocationFormFields.model_construct( allocation=[ - AllocationField.construct(area_id="e", coefficient=0.5), - AllocationField.construct(area_id="s", coefficient=0.25), - AllocationField.construct(area_id="invalid_area", coefficient=0.25), + AllocationField.model_construct(area_id="e", coefficient=0.5), + AllocationField.model_construct(area_id="s", coefficient=0.25), + AllocationField.model_construct(area_id="invalid_area", coefficient=0.25), ] ) diff --git a/tests/study/storage/rawstudy/test_raw_study_service.py b/tests/study/storage/rawstudy/test_raw_study_service.py index 4154362c41..e33c0f532d 100644 --- a/tests/study/storage/rawstudy/test_raw_study_service.py +++ b/tests/study/storage/rawstudy/test_raw_study_service.py @@ -26,7 +26,6 @@ from antarest.study.model import RawStudy, StudyAdditionalData from antarest.study.storage.patch_service import PatchService from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( - STStorageConfig, STStorageGroup, STStorageProperties, ) diff --git a/tests/study/storage/variantstudy/test_variant_study_service.py b/tests/study/storage/variantstudy/test_variant_study_service.py index a58399e097..3d078fda8a 100644 --- a/tests/study/storage/variantstudy/test_variant_study_service.py +++ b/tests/study/storage/variantstudy/test_variant_study_service.py @@ -31,7 +31,6 @@ from antarest.study.model import RawStudy, StudyAdditionalData from antarest.study.storage.patch_service import PatchService from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( - STStorageConfig, STStorageGroup, STStorageProperties, ) diff --git a/tests/study/test_field_validators.py b/tests/study/test_field_validators.py index 0f1d5414b4..cadadab556 100644 --- a/tests/study/test_field_validators.py +++ b/tests/study/test_field_validators.py @@ -9,11 +9,9 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from typing import Annotated import pytest -from mypy.exprtotype import ANNOTATED_TYPE_NAMES -from pydantic import BaseModel, Field, TypeAdapter, ValidationError +from pydantic import TypeAdapter, ValidationError from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import AreaId, ItemName diff --git a/tests/variantstudy/model/command/test_create_st_storage.py b/tests/variantstudy/model/command/test_create_st_storage.py index d07ea8e999..3450898ee4 100644 --- a/tests/variantstudy/model/command/test_create_st_storage.py +++ b/tests/variantstudy/model/command/test_create_st_storage.py @@ -20,7 +20,6 @@ from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( STStorage880Properties, - STStorageConfig, STStorageGroup, STStorageProperties, ) diff --git a/tests/variantstudy/model/command/test_remove_cluster.py b/tests/variantstudy/model/command/test_remove_cluster.py index baca7d65c1..85621874e4 100644 --- a/tests/variantstudy/model/command/test_remove_cluster.py +++ b/tests/variantstudy/model/command/test_remove_cluster.py @@ -19,9 +19,7 @@ BindingConstraintOperator, ) from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorage880Properties from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( - Thermal870Properties, ThermalClusterGroup, ThermalProperties, ) diff --git a/tests/variantstudy/model/command/test_replace_matrix.py b/tests/variantstudy/model/command/test_replace_matrix.py index c635071fab..2f27898de7 100644 --- a/tests/variantstudy/model/command/test_replace_matrix.py +++ b/tests/variantstudy/model/command/test_replace_matrix.py @@ -10,11 +10,6 @@ # # This file is part of the Antares project. -from unittest.mock import Mock, patch - -import numpy as np - -from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/model/command/test_update_comments.py b/tests/variantstudy/model/command/test_update_comments.py index c5581284f3..cfd4b4ea4b 100644 --- a/tests/variantstudy/model/command/test_update_comments.py +++ b/tests/variantstudy/model/command/test_update_comments.py @@ -10,8 +10,6 @@ # # This file is part of the Antares project. -from unittest.mock import Mock - import pytest from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy diff --git a/tests/variantstudy/model/test_variant_model.py b/tests/variantstudy/model/test_variant_model.py index 457a41ea67..3fe2af4706 100644 --- a/tests/variantstudy/model/test_variant_model.py +++ b/tests/variantstudy/model/test_variant_model.py @@ -14,7 +14,6 @@ import typing as t import uuid from pathlib import Path -from unittest.mock import patch import pytest from antares.study.version import StudyVersion diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 5e666660cd..699b4bb51c 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -13,7 +13,7 @@ import importlib import itertools import pkgutil -from typing import Any, Dict, List, Optional, Set +from typing import Any, Dict, Optional, Set from unittest.mock import Mock import pytest From 469f1468b084e938108ecc4d9c7b6c0d0cb62170 Mon Sep 17 00:00:00 2001 From: Theo Pascoli <48944759+TheoPascoli@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:21:43 +0100 Subject: [PATCH 07/11] feat(properties): add two fields to area properties form (#2347) --- antarest/study/business/areas/properties_management.py | 10 ++++++++++ tests/integration/test_integration.py | 6 ++++++ webapp/public/locales/en/main.json | 2 ++ webapp/public/locales/fr/main.json | 2 ++ .../explore/Modelization/Areas/Properties/Fields.tsx | 10 ++++++++++ .../explore/Modelization/Areas/Properties/utils.ts | 2 ++ 6 files changed, 32 insertions(+) diff --git a/antarest/study/business/areas/properties_management.py b/antarest/study/business/areas/properties_management.py index b0afda1833..28a33087fd 100644 --- a/antarest/study/business/areas/properties_management.py +++ b/antarest/study/business/areas/properties_management.py @@ -57,6 +57,8 @@ class PropertiesFormFields(FormFieldsBaseModel): non_dispatch_power: bool dispatch_hydro_power: bool other_dispatch_power: bool + spread_unsupplied_energy_cost: float + spread_spilled_energy_cost: float filter_synthesis: Set[str] filter_by_year: Set[str] # version 830 @@ -98,6 +100,14 @@ def validation(cls, values: Dict[str, Any]) -> Dict[str, Any]: "path": f"{NODAL_OPTIMIZATION_PATH}/other-dispatchable-power", "default_value": True, }, + "spread_unsupplied_energy_cost": { + "path": f"{NODAL_OPTIMIZATION_PATH}/spread-unsupplied-energy-cost", + "default_value": 0.0, + }, + "spread_spilled_energy_cost": { + "path": f"{NODAL_OPTIMIZATION_PATH}/spread-spilled-energy-cost", + "default_value": 0.0, + }, "filter_synthesis": { "path": f"{FILTERING_PATH}/filter-synthesis", "encode": encode_filter, diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index df4d67e33a..fc52af7a64 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1154,6 +1154,8 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: "nonDispatchPower": True, "dispatchHydroPower": True, "otherDispatchPower": True, + "spreadUnsuppliedEnergyCost": 0.0, + "spreadSpilledEnergyCost": 0.0, "filterSynthesis": {"hourly", "daily", "weekly", "monthly", "annual"}, "filterByYear": {"hourly", "daily", "weekly", "monthly", "annual"}, "adequacyPatchMode": "outside", @@ -1167,6 +1169,8 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: "nonDispatchPower": False, "dispatchHydroPower": False, "otherDispatchPower": False, + "spreadUnsuppliedEnergyCost": 10.0, + "spreadSpilledEnergyCost": 10.0, "filterSynthesis": ["monthly", "annual"], "filterByYear": ["hourly", "daily", "annual"], "adequacyPatchMode": "inside", @@ -1182,6 +1186,8 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: "nonDispatchPower": False, "dispatchHydroPower": False, "otherDispatchPower": False, + "spreadUnsuppliedEnergyCost": 10.0, + "spreadSpilledEnergyCost": 10.0, "filterSynthesis": {"monthly", "annual"}, "filterByYear": {"hourly", "daily", "annual"}, "adequacyPatchMode": "inside", diff --git a/webapp/public/locales/en/main.json b/webapp/public/locales/en/main.json index 17e97d6ec1..02e2a57afe 100644 --- a/webapp/public/locales/en/main.json +++ b/webapp/public/locales/en/main.json @@ -445,6 +445,8 @@ "study.modelization.properties.energyCost": "Energy cost (€/MWh)", "study.modelization.properties.unsupplied": "Unsupplied", "study.modelization.properties.spilled": "Spilled", + "study.modelization.properties.spreadUnsupplied": "Spread unsupplied", + "study.modelization.properties.spreadSpilled": "Spread spilled", "study.modelization.properties.lastResortShedding": "Last resort shedding status", "study.modelization.properties.nonDispatchPower": "Non dispatch. power", "study.modelization.properties.dispatchHydroPower": "Dispatch. hydropower", diff --git a/webapp/public/locales/fr/main.json b/webapp/public/locales/fr/main.json index c655103e6a..06f27aa593 100644 --- a/webapp/public/locales/fr/main.json +++ b/webapp/public/locales/fr/main.json @@ -445,6 +445,8 @@ "study.modelization.properties.energyCost": "Coût de l'énergie (€/MWh)", "study.modelization.properties.unsupplied": "Non distribuée", "study.modelization.properties.spilled": "Non évacuée", + "study.modelization.properties.spreadUnsupplied": "Spread non distribuée", + "study.modelization.properties.spreadSpilled": "Spread non évacuée", "study.modelization.properties.lastResortShedding": "Moyen de dernier recours pour l'écrêtement", "study.modelization.properties.nonDispatchPower": "Production non pilotable", "study.modelization.properties.dispatchHydroPower": "Hydraulique pilotable", diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/Fields.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/Fields.tsx index 6c7a56220f..ead7ebea69 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/Fields.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Properties/Fields.tsx @@ -51,6 +51,16 @@ function Fields() { label={t("study.modelization.properties.spilled")} control={control} /> + +
Date: Mon, 17 Feb 2025 13:04:07 +0100 Subject: [PATCH 08/11] fix(scan): workspace directories must not overlap (#2334) - Addition of a check on startup that not workspace is nested inside another one. - When scanning, studies are now matched on the directory AND workspace name. --- antarest/core/config.py | 15 +++++ antarest/study/service.py | 18 +++--- tests/storage/test_config.py | 114 ++++++++++++++++++++++++++++++++++ tests/storage/test_service.py | 4 +- 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 tests/storage/test_config.py diff --git a/antarest/core/config.py b/antarest/core/config.py index 8cdb3fff75..4d16da6e25 100644 --- a/antarest/core/config.py +++ b/antarest/core/config.py @@ -201,6 +201,8 @@ def from_dict(cls, data: JSON) -> "StorageConfig": if "workspaces" in data else defaults.workspaces ) + + cls._validate_workspaces(data, workspaces) return cls( matrixstore=Path(data["matrixstore"]) if "matrixstore" in data else defaults.matrixstore, archive_dir=Path(data["archive_dir"]) if "archive_dir" in data else defaults.archive_dir, @@ -225,6 +227,19 @@ def from_dict(cls, data: JSON) -> "StorageConfig": matrixstore_format=InternalMatrixFormat(data.get("matrixstore_format", defaults.matrixstore_format)), ) + @classmethod + def _validate_workspaces(cls, config_as_json: JSON, workspaces: Dict[str, WorkspaceConfig]) -> None: + """ + Validate that no two workspaces have overlapping paths. + """ + workspace_name_by_path = [(config.path, name) for name, config in workspaces.items()] + for path, name in workspace_name_by_path: + for path2, name2 in workspace_name_by_path: + if name != name2 and path.is_relative_to(path2): + raise ValueError( + f"Overlapping workspace paths found: '{name}' and '{name2}' '{path}' is relative to '{path2}' " + ) + @dataclass(frozen=True) class NbCoresConfig: diff --git a/antarest/study/service.py b/antarest/study/service.py index bf13d7fccb..b402c9330b 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -957,16 +957,17 @@ def sync_studies_on_disk( all_studies = [raw_study for raw_study in all_studies if directory in Path(raw_study.path).parents] else: all_studies = [raw_study for raw_study in all_studies if directory == Path(raw_study.path).parent] - studies_by_path = {study.path: study for study in all_studies} + studies_by_path_workspace = {(study.workspace, study.path): study for study in all_studies} # delete orphan studies on database - paths = [str(f.path) for f in folders] + # key should be workspace, path to sync correctly studies with same path in different workspace + workspace_paths = [(f.workspace, str(f.path)) for f in folders] for study in all_studies: if ( isinstance(study, RawStudy) and not study.archived - and (study.workspace != DEFAULT_WORKSPACE_NAME and study.path not in paths) + and (study.workspace != DEFAULT_WORKSPACE_NAME and (study.workspace, study.path) not in workspace_paths) ): if not study.missing: logger.info( @@ -993,11 +994,12 @@ def sync_studies_on_disk( self.repository.delete(study.id) # Add new studies - study_paths = [study.path for study in all_studies if study.missing is None] + study_paths = [(study.workspace, study.path) for study in all_studies if study.missing is None] missing_studies = {study.path: study for study in all_studies if study.missing is not None} for folder in folders: study_path = str(folder.path) - if study_path not in study_paths: + workspace = folder.workspace + if (workspace, study_path) not in study_paths: try: if study_path not in missing_studies.keys(): base_path = self.config.storage.workspaces[folder.workspace].path @@ -1007,7 +1009,7 @@ def sync_studies_on_disk( name=folder.path.name, path=study_path, folder=str(dir_name), - workspace=folder.workspace, + workspace=workspace, owner=None, groups=folder.groups, public_mode=PublicMode.FULL if len(folder.groups) == 0 else PublicMode.NONE, @@ -1042,8 +1044,8 @@ def sync_studies_on_disk( ) except Exception as e: logger.error(f"Failed to add study {folder.path}", exc_info=e) - elif directory and study_path in studies_by_path: - existing_study = studies_by_path[study_path] + elif directory and (workspace, study_path) in studies_by_path_workspace: + existing_study = studies_by_path_workspace[(workspace, study_path)] if self.storage_service.raw_study_service.update_name_and_version_from_raw_meta(existing_study): self.repository.save(existing_study) diff --git a/tests/storage/test_config.py b/tests/storage/test_config.py new file mode 100644 index 0000000000..1db3f23754 --- /dev/null +++ b/tests/storage/test_config.py @@ -0,0 +1,114 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +from pathlib import Path + +import pytest + +from antarest.core.config import InternalMatrixFormat, StorageConfig + + +@pytest.fixture +def storage_config_default(): + return { + "matrixstore": "./custom_matrixstore", + "archive_dir": "./custom_archives", + "tmp_dir": "./custom_tmp", + "allow_deletion": True, + "watcher_lock": False, + "watcher_lock_delay": 20, + "download_default_expiration_timeout_minutes": 2880, + "matrix_gc_sleeping_time": 7200, + "matrix_gc_dry_run": True, + "auto_archive_threshold_days": 120, + "auto_archive_dry_run": True, + "auto_archive_sleeping_time": 7200, + "auto_archive_max_parallel": 10, + "snapshot_retention_days": 14, + "matrixstore_format": "tsv", + } + + +def test_storage_config_from_dict(storage_config_default): + data = { + **storage_config_default, + "workspaces": { + "workspace1": { + "path": "./workspace1", + }, + "workspace2": { + "path": "./workspace2", + }, + }, + } + + config = StorageConfig.from_dict(data) + + assert config.matrixstore == Path("./custom_matrixstore") + assert config.archive_dir == Path("./custom_archives") + assert config.tmp_dir == Path("./custom_tmp") + assert config.workspaces["workspace1"].path == Path("./workspace1") + assert config.workspaces["workspace2"].path == Path("./workspace2") + assert config.allow_deletion is True + assert config.watcher_lock is False + assert config.watcher_lock_delay == 20 + assert config.download_default_expiration_timeout_minutes == 2880 + assert config.matrix_gc_sleeping_time == 7200 + assert config.matrix_gc_dry_run is True + assert config.auto_archive_threshold_days == 120 + assert config.auto_archive_dry_run is True + assert config.auto_archive_sleeping_time == 7200 + assert config.auto_archive_max_parallel == 10 + assert config.snapshot_retention_days == 14 + assert config.matrixstore_format == InternalMatrixFormat.TSV + + +def test_storage_config_from_dict_validiation_errors(storage_config_default): + data = { + **storage_config_default, + "workspaces": { + "workspace1": { + "path": "./a/workspace1", + }, + "workspace2": { + "path": "./a/", + }, + }, + } + + with pytest.raises(ValueError): + StorageConfig.from_dict(data) + + data = { + **storage_config_default, + "workspaces": { + "workspace1": { + "path": "./a/", + }, + "workspace2": { + "path": "./a/workspace2", + }, + }, + } + + with pytest.raises(ValueError): + StorageConfig.from_dict(data) + + data = { + **storage_config_default, + "workspaces": { + "workspace1": {"path": "./a/", "some_other_config": "value1"}, + "workspace2": {"path": "./a/", "some_other_config": "value2"}, + }, + } + + with pytest.raises(ValueError): + StorageConfig.from_dict(data) diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index 58af2370b6..05ce1550a0 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -259,8 +259,8 @@ def test_study_listing(db_session: Session) -> None: @pytest.mark.unit_test def test_sync_studies_from_disk() -> None: now = datetime.utcnow() - ma = RawStudy(id="a", path="a") - fa = StudyFolder(path=Path("a"), workspace="", groups=[]) + ma = RawStudy(id="a", path="a", workspace="workspace1") + fa = StudyFolder(path=Path("a"), workspace="workspace1", groups=[]) mb = RawStudy(id="b", path="b") mc = RawStudy( id="c", From e657e7d2fa78763ea59525d0a40f265ff5d408ed Mon Sep 17 00:00:00 2001 From: Hatim Dinia <33469289+hdinia@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:33:16 +0100 Subject: [PATCH 09/11] fix(ui-debug): treat 'file://*.json' files as JSON instead of unsupported (#2346) --- webapp/src/components/App/Singlestudy/explore/Debug/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts index 2fac2890ea..053c9a7d43 100644 --- a/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Debug/utils.ts @@ -65,7 +65,7 @@ const URL_SCHEMES = { FILE: "file://", } as const; -const SUPPORTED_EXTENSIONS = [".txt", ".log", ".csv", ".tsv", ".ini", ".yml"] as const; +const SUPPORTED_EXTENSIONS = [".txt", ".log", ".csv", ".tsv", ".ini", ".yml", ".json"] as const; // Maps file types to their corresponding icon components. const iconByFileType: Record = { From 6c8b7952b6bc14ed0b0b76c7b1e4855b9a10bb5c Mon Sep 17 00:00:00 2001 From: Hatim Dinia <33469289+hdinia@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:42:57 +0100 Subject: [PATCH 10/11] feat(ui-matrix): enable flexible row count configuration (#2345) --- .../explore/Modelization/Areas/Hydro/HydroMatrix.tsx | 1 + .../explore/Modelization/Areas/Hydro/utils.ts | 10 +++++++++- .../components/common/Matrix/hooks/useMatrix/index.ts | 6 +++++- webapp/src/components/common/Matrix/index.tsx | 5 ++++- webapp/src/components/common/Matrix/shared/types.ts | 1 + 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx index e367d7cb4b..38268c1db8 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/HydroMatrix.tsx @@ -43,6 +43,7 @@ function HydroMatrix({ type }: Props) { readOnly={hydroMatrix.readOnly} showPercent={hydroMatrix.showPercent} fetchMatrixData={hydroMatrix.fetchFn} + rowCountSource={hydroMatrix.rowCountSource} /> ); diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts index 75ba126ef9..3d39f24ff5 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/Areas/Hydro/utils.ts @@ -12,7 +12,11 @@ * This file is part of the Antares project. */ -import type { MatrixDataDTO, AggregateConfig } from "../../../../../../common/Matrix/shared/types"; +import type { + MatrixDataDTO, + AggregateConfig, + RowCountSource, +} from "../../../../../../common/Matrix/shared/types"; import type { SplitViewProps } from "../../../../../../common/SplitView"; import { getAllocationMatrix } from "./Allocation/utils"; import { getCorrelationMatrix } from "./Correlation/utils"; @@ -53,6 +57,7 @@ export interface HydroMatrixProps { dateTimeColumn?: boolean; readOnly?: boolean; showPercent?: boolean; + rowCountSource?: RowCountSource; } type Matrices = Record; @@ -128,6 +133,7 @@ export const MATRICES: Matrices = { url: "input/hydro/common/capacity/creditmodulations_{areaId}", columns: generateColumns("%"), rowHeaders: ["Generating Power", "Pumping Power"], + rowCountSource: "dataLength", dateTimeColumn: false, }, [HydroMatrix.EnergyCredits]: { @@ -139,6 +145,7 @@ export const MATRICES: Matrices = { "Pumping Max Power (MW)", "Pumping Max Energy (Hours at Pmax)", ], + rowCountSource: "dataLength", }, [HydroMatrix.ReservoirLevels]: { title: "Reservoir Levels", @@ -187,6 +194,7 @@ export const MATRICES: Matrices = { "November", "December", ], + rowCountSource: "dataLength", dateTimeColumn: false, }, [HydroMatrix.Allocation]: { diff --git a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts index 7c70c8b35c..3a32082426 100644 --- a/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts +++ b/webapp/src/components/common/Matrix/hooks/useMatrix/index.ts @@ -27,6 +27,7 @@ import type { MatrixUpdateDTO, MatrixAggregates, AggregateConfig, + RowCountSource, } from "../../shared/types"; import { calculateMatrixAggregates, @@ -59,6 +60,7 @@ export function useMatrix( customColumns?: string[] | readonly string[], colWidth?: number, fetchMatrixData?: fetchMatrixFn, + rowCountSource: RowCountSource = "matrixIndex", ) { const enqueueErrorSnackbar = useEnqueueErrorSnackbar(); const [columnCount, setColumnCount] = useState(0); @@ -312,6 +314,8 @@ export function useMatrix( // Use the matrix index 'steps' field to determine the number of rows // This ensures consistent row display (8760 for hourly, 365 for daily/weekly) // rather than using data.length which can vary for Binding Constraints (8784/366) - rowCount: index?.steps, + // !Bugfix: some matrices have a fixed number of rows regardless of the time stamps + // for those we want to use the data.length instead of the matrix index via `rowCountSource` prop + rowCount: rowCountSource === "matrixIndex" ? index?.steps : currentState.data.length, }; } diff --git a/webapp/src/components/common/Matrix/index.tsx b/webapp/src/components/common/Matrix/index.tsx index 8d43a8cd53..274319f485 100644 --- a/webapp/src/components/common/Matrix/index.tsx +++ b/webapp/src/components/common/Matrix/index.tsx @@ -23,7 +23,7 @@ import { MatrixContainer, MatrixHeader, MatrixTitle } from "./styles"; import MatrixActions from "./components/MatrixActions"; import EmptyView from "../page/EmptyView"; import type { fetchMatrixFn } from "../../App/Singlestudy/explore/Modelization/Areas/Hydro/utils"; -import type { AggregateConfig } from "./shared/types"; +import type { AggregateConfig, RowCountSource } from "./shared/types"; import GridOffIcon from "@mui/icons-material/GridOff"; import MatrixUpload from "@/components/common/Matrix/components/MatrixUpload"; @@ -41,6 +41,7 @@ interface MatrixProps { colWidth?: number; fetchMatrixData?: fetchMatrixFn; canImport?: boolean; + rowCountSource?: RowCountSource; } function Matrix({ @@ -57,6 +58,7 @@ function Matrix({ colWidth, fetchMatrixData, canImport = false, + rowCountSource, }: MatrixProps) { const { t } = useTranslation(); const { study } = useOutletContext<{ study: StudyMetadata }>(); @@ -92,6 +94,7 @@ function Matrix({ customColumns, colWidth, fetchMatrixData, + rowCountSource, ); //////////////////////////////////////////////////////////////// diff --git a/webapp/src/components/common/Matrix/shared/types.ts b/webapp/src/components/common/Matrix/shared/types.ts index b09a7a75fd..3c16099396 100644 --- a/webapp/src/components/common/Matrix/shared/types.ts +++ b/webapp/src/components/common/Matrix/shared/types.ts @@ -23,6 +23,7 @@ export type TimeFrequencyType = (typeof TimeFrequency)[keyof typeof TimeFrequenc export type DateIncrementFunction = (date: Date, amount: number) => Date; export type FormatFunction = (date: Date, firstWeekSize: number) => string; +export type RowCountSource = "matrixIndex" | "dataLength"; // !NOTE: This is temporary, date/time array should be generated by the API export interface DateTimeMetadataDTO { From 0bffadb1e943dcd846c2cd49086d6aafadf16bf9 Mon Sep 17 00:00:00 2001 From: Sylvain Leclerc Date: Tue, 18 Feb 2025 14:35:11 +0100 Subject: [PATCH 11/11] refactor(services): fix wrong dependency from business to service (#2343) Signed-off-by: Sylvain Leclerc --- antarest/matrixstore/in_memory.py | 65 +++ .../business/adequacy_patch_management.py | 24 +- .../advanced_parameters_management.py | 25 +- .../study/business/allocation_management.py | 30 +- antarest/study/business/area_management.py | 150 +++---- .../study/business/areas/hydro_management.py | 34 +- .../business/areas/properties_management.py | 29 +- .../business/areas/renewable_management.py | 104 ++--- .../business/areas/st_storage_management.py | 144 +++---- .../business/areas/thermal_management.py | 119 +++--- .../business/binding_constraint_management.py | 178 ++++---- antarest/study/business/config_management.py | 24 +- .../study/business/correlation_management.py | 40 +- antarest/study/business/district_manager.py | 43 +- antarest/study/business/general_management.py | 41 +- antarest/study/business/link_management.py | 55 ++- antarest/study/business/matrix_management.py | 33 +- .../study/business/optimization_management.py | 24 +- .../study/business/playlist_management.py | 38 +- .../business/scenario_builder_management.py | 40 +- antarest/study/business/study_interface.py | 119 ++++++ .../study/business/table_mode_management.py | 22 +- .../business/thematic_trimming_management.py | 41 +- .../business/timeseries_config_management.py | 23 +- .../study/business/xpansion_management.py | 64 +-- antarest/study/main.py | 2 +- antarest/study/model.py | 6 +- antarest/study/service.py | 273 ++++++++++--- .../storage/variantstudy/command_factory.py | 2 - .../command/create_binding_constraint.py | 4 +- .../variantstudy/model/command_context.py | 1 - .../variantstudy/variant_study_service.py | 3 +- antarest/study/web/study_data_blueprint.py | 278 ++++++++----- .../study/web/xpansion_studies_blueprint.py | 14 +- tests/conftest_services.py | 3 - .../test_in_memory_matrix_service.py | 32 ++ .../test_matrix_garbage_collector.py | 1 - tests/storage/business/conftest.py | 47 +++ .../storage/business/test_arealink_manager.py | 305 ++------------ tests/storage/business/test_config_manager.py | 26 +- .../test_timeseries_config_manager.py | 18 +- tests/storage/business/test_watcher.py | 1 + .../storage/business/test_xpansion_manager.py | 331 ++++++++------- tests/storage/test_service.py | 1 + .../areas/test_st_storage_management.py | 383 +++++------------- .../business/areas/test_thermal_management.py | 378 ++++++++--------- tests/study/business/conftest.py | 51 +++ .../study/business/test_allocation_manager.py | 237 +++++------ .../business/test_correlation_manager.py | 200 ++++----- tests/study/business/test_district_manager.py | 132 +++--- tests/variantstudy/conftest.py | 4 - tests/variantstudy/test_command_factory.py | 2 - 52 files changed, 2033 insertions(+), 2211 deletions(-) create mode 100644 antarest/matrixstore/in_memory.py create mode 100644 antarest/study/business/study_interface.py create mode 100644 tests/matrixstore/test_in_memory_matrix_service.py create mode 100644 tests/storage/business/conftest.py create mode 100644 tests/study/business/conftest.py diff --git a/antarest/matrixstore/in_memory.py b/antarest/matrixstore/in_memory.py new file mode 100644 index 0000000000..c173ebc915 --- /dev/null +++ b/antarest/matrixstore/in_memory.py @@ -0,0 +1,65 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +import hashlib +import time +from typing import Dict, List, Optional + +import numpy as np +import numpy.typing as npt +from typing_extensions import override + +from antarest.matrixstore.model import MatrixData, MatrixDTO +from antarest.matrixstore.service import ISimpleMatrixService + + +class InMemorySimpleMatrixService(ISimpleMatrixService): + """ + In memory implementation of matrix service, for unit testing purposes. + """ + + def __init__(self) -> None: + self._content: Dict[str, MatrixDTO] = {} + + def _make_dto(self, id: str, matrix: npt.NDArray[np.float64]) -> MatrixDTO: + matrix = matrix.reshape((1, 0)) if matrix.size == 0 else matrix + data = matrix.tolist() + index = [str(i) for i in range(matrix.shape[0])] + columns = [str(i) for i in range(matrix.shape[1])] + return MatrixDTO( + data=data, + index=index, + columns=columns, + id=id, + created_at=int(time.time()), + width=len(columns), + height=len(index), + ) + + @override + def create(self, data: List[List[MatrixData]] | npt.NDArray[np.float64]) -> str: + matrix = data if isinstance(data, np.ndarray) else np.array(data, dtype=np.float64) + matrix_hash = hashlib.sha256(matrix.data).hexdigest() + self._content[matrix_hash] = self._make_dto(matrix_hash, matrix) + return matrix_hash + + @override + def get(self, matrix_id: str) -> Optional[MatrixDTO]: + return self._content.get(matrix_id, None) + + @override + def exists(self, matrix_id: str) -> bool: + return matrix_id in self._content + + @override + def delete(self, matrix_id: str) -> None: + del self._content[matrix_id] diff --git a/antarest/study/business/adequacy_patch_management.py b/antarest/study/business/adequacy_patch_management.py index 9ad3bd1c62..d3f38ae4a0 100644 --- a/antarest/study/business/adequacy_patch_management.py +++ b/antarest/study/business/adequacy_patch_management.py @@ -16,10 +16,11 @@ from antarest.study.business.all_optional_meta import all_optional_model from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import STUDY_VERSION_8_3, STUDY_VERSION_8_5, Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel +from antarest.study.model import STUDY_VERSION_8_3, STUDY_VERSION_8_5 from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class PriceTakingOrder(EnumIgnoreCase): @@ -98,14 +99,14 @@ class AdequacyPatchFormFields(FormFieldsBaseModel): class AdequacyPatchManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_field_values(self, study: Study) -> AdequacyPatchFormFields: + def get_field_values(self, study: StudyInterface) -> AdequacyPatchFormFields: """ Get adequacy patch field values for the webapp form """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() general_data = file_study.tree.get(GENERAL_DATA_PATH.split("/")) parent = general_data.get("adequacy patch", {}) @@ -113,13 +114,13 @@ def get_value(field_info: FieldInfo) -> Any: path = field_info["path"] start_version = field_info.get("start_version", -1) target_name = path.split("/")[-1] - is_in_version = file_study.config.version >= start_version + is_in_version = study.version >= start_version return parent.get(target_name, field_info["default_value"]) if is_in_version else None return AdequacyPatchFormFields.model_construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) - def set_field_values(self, study: Study, field_values: AdequacyPatchFormFields) -> None: + def set_field_values(self, study: StudyInterface, field_values: AdequacyPatchFormFields) -> None: """ Set adequacy patch config from the webapp form """ @@ -133,11 +134,10 @@ def set_field_values(self, study: Study, field_values: AdequacyPatchFormFields) UpdateConfig( target=info["path"], data=value, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study.version, ) ) if commands: - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) diff --git a/antarest/study/business/advanced_parameters_management.py b/antarest/study/business/advanced_parameters_management.py index 0b5c9dc809..22193a6890 100644 --- a/antarest/study/business/advanced_parameters_management.py +++ b/antarest/study/business/advanced_parameters_management.py @@ -12,17 +12,17 @@ from typing import Any, Dict, List -from antares.study.version import StudyVersion from pydantic import field_validator from pydantic.types import StrictInt, StrictStr from antarest.core.exceptions import InvalidFieldForVersionError from antarest.study.business.all_optional_meta import all_optional_model from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import STUDY_VERSION_8_8, Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel +from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class InitialReservoirLevel(EnumIgnoreCase): @@ -216,14 +216,14 @@ def check_accuracy_on_correlation(cls, v: str) -> str: class AdvancedParamsManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_field_values(self, study: Study) -> AdvancedParamsFormFields: + def get_field_values(self, study: StudyInterface) -> AdvancedParamsFormFields: """ Get Advanced parameters values for the webapp form """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() general_data = file_study.tree.get(GENERAL_DATA_PATH.split("/")) advanced_params = general_data.get("advanced parameters", {}) other_preferences = general_data.get("other preferences", {}) @@ -242,7 +242,7 @@ def get_value(field_info: FieldInfo) -> Any: return AdvancedParamsFormFields.model_construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) - def set_field_values(self, study: Study, field_values: AdvancedParamsFormFields) -> None: + def set_field_values(self, study: StudyInterface, field_values: AdvancedParamsFormFields) -> None: """ Set Advanced parameters values from the webapp form """ @@ -256,7 +256,7 @@ def set_field_values(self, study: Study, field_values: AdvancedParamsFormFields) if ( field_name == "unit_commitment_mode" and value == UnitCommitmentMode.MILP - and StudyVersion.parse(study.version) < STUDY_VERSION_8_8 + and study.version < STUDY_VERSION_8_8 ): raise InvalidFieldForVersionError("Unit commitment mode `MILP` only exists in v8.8+ studies") @@ -264,11 +264,10 @@ def set_field_values(self, study: Study, field_values: AdvancedParamsFormFields) UpdateConfig( target=info["path"], data=value, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study.version, ) ) if len(commands) > 0: - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) diff --git a/antarest/study/business/allocation_management.py b/antarest/study/business/allocation_management.py index 1d2950b0e9..3bc182c16c 100644 --- a/antarest/study/business/allocation_management.py +++ b/antarest/study/business/allocation_management.py @@ -20,10 +20,10 @@ from antarest.core.exceptions import AllocationDataNotFound, AreaNotFound from antarest.study.business.model.area_model import AreaInfoDTO -from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import FormFieldsBaseModel from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class AllocationField(FormFieldsBaseModel): @@ -114,10 +114,10 @@ class AllocationManager: Manage hydraulic allocation coefficients. """ - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_allocation_data(self, study: Study, area_id: str) -> Dict[str, float]: + def get_allocation_data(self, study: StudyInterface, area_id: str) -> Dict[str, float]: """ Get hydraulic allocation data. @@ -133,7 +133,7 @@ def get_allocation_data(self, study: Study, area_id: str) -> Dict[str, float]: """ # sourcery skip: reintroduce-else, swap-if-else-branches, use-named-expression - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() allocation_data = file_study.tree.get(f"input/hydro/allocation/{area_id}".split("/"), depth=2) if not allocation_data: @@ -142,7 +142,7 @@ def get_allocation_data(self, study: Study, area_id: str) -> Dict[str, float]: return allocation_data.get("[allocation]", {}) # type: ignore def get_allocation_form_fields( - self, all_areas: List[AreaInfoDTO], study: Study, area_id: str + self, all_areas: List[AreaInfoDTO], study: StudyInterface, area_id: str ) -> AllocationFormFields: """ Get hydraulic allocation coefficients. @@ -172,7 +172,7 @@ def get_allocation_form_fields( def set_allocation_form_fields( self, all_areas: List[AreaInfoDTO], - study: Study, + study: StudyInterface, area_id: str, data: AllocationFormFields, ) -> AllocationFormFields: @@ -198,16 +198,14 @@ def set_allocation_form_fields( filtered_allocations = [f for f in data.allocation if f.coefficient > 0 and f.area_id in areas_ids] - command_context = self.storage_service.variant_study_service.command_factory.command_context - file_study = self.storage_service.get_storage(study).get_raw(study) command = UpdateConfig( target=f"input/hydro/allocation/{area_id}/[allocation]", data={f.area_id: f.coefficient for f in filtered_allocations}, - command_context=command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) updated_allocations = self.get_allocation_data(study, area_id) @@ -218,7 +216,7 @@ def set_allocation_form_fields( ] ) - def get_allocation_matrix(self, study: Study, all_areas: List[AreaInfoDTO]) -> AllocationMatrix: + def get_allocation_matrix(self, study: StudyInterface, all_areas: List[AreaInfoDTO]) -> AllocationMatrix: """ Get the hydraulic allocation matrix for all areas in the study. @@ -233,7 +231,7 @@ def get_allocation_matrix(self, study: Study, all_areas: List[AreaInfoDTO]) -> A AllocationDataNotFound: if the allocation data is not found. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() allocation_cfg = file_study.tree.get(["input", "hydro", "allocation"], depth=3) if not allocation_cfg: diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 9d83e9be7a..5a3f45edcc 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -25,10 +25,8 @@ LayerInfoDTO, UpdateAreaUi, ) -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import Patch, PatchArea, PatchCluster, RawStudy, Study -from antarest.study.repository import StudyMetadataRepository -from antarest.study.storage.patch_service import PatchService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import Patch, PatchArea, PatchCluster from antarest.study.storage.rawstudy.model.filesystem.config.area import ( AreaFolder, ThermalAreasProperties, @@ -37,12 +35,12 @@ from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, DistrictSet from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.update_area_ui import UpdateAreaUI from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext logger = logging.getLogger(__name__) @@ -95,31 +93,19 @@ def _get_area_layers(area_uis: Dict[str, Any], area: str) -> List[str]: class AreaManager: """ Manages operations related to areas in a study, including retrieval, creation, and updates. - - Attributes: - storage_service: The service responsible for study storage operations. - patch_service: The service responsible for study patch operations. - This service is used to store additional data for each area, in particular the country - of origin (`country`) and a list of tags for searching (`tags`). """ def __init__( self, - storage_service: StudyStorageService, - repository: StudyMetadataRepository, + command_context: CommandContext, ) -> None: """ Initializes the AreaManager. - - Args: - storage_service: The service responsible for study storage operations. - repository: The repository for study metadata operations. """ - self.storage_service = storage_service - self.patch_service = PatchService(repository=repository) + self._command_context = command_context # noinspection SpellCheckingInspection - def get_all_area_props(self, study: RawStudy) -> Mapping[str, AreaOutput]: + def get_all_area_props(self, study: StudyInterface) -> Mapping[str, AreaOutput]: """ Retrieves all areas of a study. @@ -130,7 +116,7 @@ def get_all_area_props(self, study: RawStudy) -> Mapping[str, AreaOutput]: Raises: ConfigFileNotFound: if a configuration file is not found. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() # Get the area information from the `/input/areas/` file. path = _ALL_AREAS_PATH @@ -167,7 +153,7 @@ def get_all_area_props(self, study: RawStudy) -> Mapping[str, AreaOutput]: # noinspection SpellCheckingInspection def update_areas_props( - self, study: RawStudy, update_areas_by_ids: Mapping[str, AreaOutput] + self, study: StudyInterface, update_areas_by_ids: Mapping[str, AreaOutput] ) -> Mapping[str, AreaOutput]: """ Update the properties of ares. @@ -184,7 +170,7 @@ def update_areas_props( # Prepare the commands to update the thermal clusters. commands = [] - command_context = self.storage_service.variant_study_service.command_factory.command_context + command_context = self._command_context for area_id, update_area in update_areas_by_ids.items(): # Update the area properties. @@ -233,8 +219,7 @@ def update_areas_props( ) ) - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) return new_areas_by_ids @@ -242,7 +227,7 @@ def update_areas_props( def get_table_schema() -> JSON: return AreaOutput.model_json_schema() - def get_all_areas(self, study: RawStudy, area_type: Optional[AreaType] = None) -> List[AreaInfoDTO]: + def get_all_areas(self, study: StudyInterface, area_type: Optional[AreaType] = None) -> List[AreaInfoDTO]: """ Retrieves all areas and districts of a raw study based on the area type. @@ -253,9 +238,8 @@ def get_all_areas(self, study: RawStudy, area_type: Optional[AreaType] = None) - Returns: A list of area/district information. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) - metadata = self.patch_service.get(study) + file_study = study.get_files() + metadata = study.get_patch_data() areas_metadata: Dict[str, PatchArea] = metadata.areas or {} cfg_areas: Dict[str, Area] = file_study.config.areas result: List[AreaInfoDTO] = [] @@ -287,7 +271,7 @@ def get_all_areas(self, study: RawStudy, area_type: Optional[AreaType] = None) - return result - def get_all_areas_ui_info(self, study: RawStudy) -> Dict[str, Any]: + def get_all_areas_ui_info(self, study: StudyInterface) -> Dict[str, Any]: """ Retrieve information about all areas' user interface (UI) from the study. @@ -300,14 +284,12 @@ def get_all_areas_ui_info(self, study: RawStudy) -> Dict[str, Any]: Raises: ChildNotFoundError: if one of the Area IDs is not found in the configuration. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + file_study = study.get_files() area_ids = list(file_study.config.areas) return _get_ui_info_map(file_study, area_ids) - def get_layers(self, study: RawStudy) -> List[LayerInfoDTO]: - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + def get_layers(self, study: StudyInterface) -> List[LayerInfoDTO]: + file_study = study.get_files() area_ids = list(file_study.config.areas) ui_info_map = _get_ui_info_map(file_study, area_ids) layers = file_study.tree.get(["layers", "layers", "layers"]) @@ -328,9 +310,9 @@ def get_layers(self, study: RawStudy) -> List[LayerInfoDTO]: for layer in layers ] - def update_layer_areas(self, study: RawStudy, layer_id: str, areas: List[str]) -> None: + def update_layer_areas(self, study: StudyInterface, layer_id: str, areas: List[str]) -> None: logger.info(f"Updating layer {layer_id} with areas {areas}") - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() layers = file_study.tree.get(["layers", "layers", "layers"]) if layer_id not in [str(layer) for layer in list(layers.keys())]: raise LayerNotFound @@ -352,20 +334,20 @@ def create_update_commands(area_id: str) -> List[ICommand]: UpdateConfig( target=f"input/areas/{area_id}/ui/layerX", data=areas_ui[area_id]["layerX"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ), UpdateConfig( target=f"input/areas/{area_id}/ui/layerY", data=areas_ui[area_id]["layerY"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ), UpdateConfig( target=f"input/areas/{area_id}/ui/ui/layers", data=areas_ui[area_id]["ui"]["layers"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ), ] @@ -390,45 +372,45 @@ def create_update_commands(area_id: str) -> List[ICommand]: areas_ui[area]["ui"]["layers"] = " ".join(area_to_add_layers + [layer_id]) commands.extend(create_update_commands(area)) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) - def update_layer_name(self, study: RawStudy, layer_id: str, layer_name: str) -> None: + def update_layer_name(self, study: StudyInterface, layer_id: str, layer_name: str) -> None: logger.info(f"Updating layer {layer_id} with name {layer_name}") - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() layers = file_study.tree.get(["layers", "layers", "layers"]) if layer_id not in [str(layer) for layer in list(layers.keys())]: raise LayerNotFound command = UpdateConfig( target=f"layers/layers/layers/{layer_id}", data=layer_name, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) - def create_layer(self, study: RawStudy, layer_name: str) -> str: - file_study = self.storage_service.get_storage(study).get_raw(study) + def create_layer(self, study: StudyInterface, layer_name: str) -> str: + file_study = study.get_files() layers = file_study.tree.get(["layers", "layers", "layers"]) - command_context = self.storage_service.variant_study_service.command_factory.command_context + command_context = self._command_context new_id = max((int(layer) for layer in layers), default=0) + 1 if new_id == 1: command = UpdateConfig( target="layers/layers/layers", data={"0": "All", "1": layer_name}, command_context=command_context, - study_version=file_study.config.version, + study_version=study.version, ) else: command = UpdateConfig( target=f"layers/layers/layers/{new_id}", data=layer_name, command_context=command_context, - study_version=file_study.config.version, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) return str(new_id) - def remove_layer(self, study: RawStudy, layer_id: str) -> None: + def remove_layer(self, study: StudyInterface, layer_id: str) -> None: """ Remove a layer from a study. @@ -439,7 +421,7 @@ def remove_layer(self, study: RawStudy, layer_id: str) -> None: if layer_id == "0": raise LayerNotAllowedToBeDeleted - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() layers = file_study.tree.get(["layers", "layers", "layers"]) if layer_id not in layers: @@ -450,13 +432,13 @@ def remove_layer(self, study: RawStudy, layer_id: str) -> None: command = UpdateConfig( target="layers/layers/layers", data=layers, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) - def create_area(self, study: Study, area_creation_info: AreaCreationDTO) -> AreaInfoDTO: - file_study = self.storage_service.get_storage(study).get_raw(study) + def create_area(self, study: StudyInterface, area_creation_info: AreaCreationDTO) -> AreaInfoDTO: + file_study = study.get_files() # check if area already exists area_id = transform_name_to_id(area_creation_info.name) @@ -466,16 +448,16 @@ def create_area(self, study: Study, area_creation_info: AreaCreationDTO) -> Area # Create area and apply changes in the study command = CreateArea( area_name=area_creation_info.name, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) # Update metadata - patch = self.patch_service.get(study) + patch = study.get_patch_data() patch.areas = patch.areas or {} patch.areas[area_id] = area_creation_info.metadata or PatchArea() - self.patch_service.save(study, patch) + study.update_patch_data(patch) return AreaInfoDTO( id=area_id, name=area_creation_info.name, @@ -487,16 +469,16 @@ def create_area(self, study: Study, area_creation_info: AreaCreationDTO) -> Area def update_area_metadata( self, - study: Study, + study: StudyInterface, area_id: str, area_metadata: PatchArea, ) -> AreaInfoDTO: - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() area_or_set = file_study.config.areas.get(area_id) or file_study.config.sets.get(area_id) - patch = self.patch_service.get(study) + patch = study.get_patch_data() patch.areas = patch.areas or {} patch.areas[area_id] = area_metadata - self.patch_service.save(study, patch) + study.update_patch_data(patch) return AreaInfoDTO( id=area_id, name=area_or_set.name if area_or_set is not None else area_id, @@ -505,31 +487,30 @@ def update_area_metadata( set=area_or_set.get_areas(list(file_study.config.areas)) if isinstance(area_or_set, DistrictSet) else [], ) - def update_area_ui(self, study: Study, area_id: str, area_ui: UpdateAreaUi, layer: str) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) + def update_area_ui(self, study: StudyInterface, area_id: str, area_ui: UpdateAreaUi, layer: str) -> None: command = UpdateAreaUI( area_id=area_id, area_ui=area_ui, layer=layer, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def update_thermal_cluster_metadata( self, - study: Study, + study: StudyInterface, area_id: str, clusters_metadata: Dict[str, PatchCluster], ) -> AreaInfoDTO: - file_study = self.storage_service.get_storage(study).get_raw(study) - patch = self.patch_service.get(study) + file_study = study.get_files() + patch = study.get_patch_data() patch.areas = patch.areas or {} patch.thermal_clusters = patch.thermal_clusters or {} patch.thermal_clusters.update({f"{area_id}.{tid}": clusters_metadata[tid] for tid in clusters_metadata}) - self.patch_service.save(study, patch) + study.update_patch_data(patch) return AreaInfoDTO( id=area_id, name=file_study.config.areas[area_id].name, @@ -539,14 +520,13 @@ def update_thermal_cluster_metadata( set=None, ) - def delete_area(self, study: Study, area_id: str) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) + def delete_area(self, study: StudyInterface, area_id: str) -> None: command = RemoveArea( id=area_id, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) @staticmethod def _update_with_cluster_metadata( diff --git a/antarest/study/business/areas/hydro_management.py b/antarest/study/business/areas/hydro_management.py index 6a43eaeaad..f13d2123b6 100644 --- a/antarest/study/business/areas/hydro_management.py +++ b/antarest/study/business/areas/hydro_management.py @@ -15,10 +15,10 @@ from pydantic import Field from antarest.study.business.all_optional_meta import all_optional_model -from antarest.study.business.utils import FieldInfo, FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import FieldInfo, FormFieldsBaseModel from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext INFLOW_PATH = "input/hydro/prepro/{area_id}/prepro/prepro" @@ -108,14 +108,14 @@ class ManagementOptionsFormFields(FormFieldsBaseModel): class HydroManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_field_values(self, study: Study, area_id: str) -> ManagementOptionsFormFields: + def get_field_values(self, study: StudyInterface, area_id: str) -> ManagementOptionsFormFields: """ Get management options for a given area """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() hydro_config = file_study.tree.get(HYDRO_PATH.split("/")) def get_value(field_info: FieldInfo) -> Any: @@ -129,7 +129,7 @@ def get_value(field_info: FieldInfo) -> Any: def set_field_values( self, - study: Study, + study: StudyInterface, field_values: ManagementOptionsFormFields, area_id: str, ) -> None: @@ -146,17 +146,16 @@ def set_field_values( UpdateConfig( target="/".join([info["path"], area_id]), data=value, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study.version, ) ) if len(commands) > 0: - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) # noinspection SpellCheckingInspection - def get_inflow_structure(self, study: Study, area_id: str) -> InflowStructure: + def get_inflow_structure(self, study: StudyInterface, area_id: str) -> InflowStructure: """ Retrieves inflow structure values for a specific area within a study. @@ -165,12 +164,12 @@ def get_inflow_structure(self, study: Study, area_id: str) -> InflowStructure: """ # NOTE: Focusing on the single field "intermonthly-correlation" due to current model scope. path = INFLOW_PATH.format(area_id=area_id) - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() inter_monthly_correlation = file_study.tree.get(path.split("/")).get("intermonthly-correlation", 0.5) return InflowStructure(inter_monthly_correlation=inter_monthly_correlation) # noinspection SpellCheckingInspection - def update_inflow_structure(self, study: Study, area_id: str, values: InflowStructure) -> None: + def update_inflow_structure(self, study: StudyInterface, area_id: str, values: InflowStructure) -> None: """ Updates inflow structure values for a specific area within a study. @@ -184,11 +183,10 @@ def update_inflow_structure(self, study: Study, area_id: str, values: InflowStru """ # NOTE: Updates only "intermonthly-correlation" due to current model scope. path = INFLOW_PATH.format(area_id=area_id) - file_study = self.storage_service.get_storage(study).get_raw(study) command = UpdateConfig( target=path, data={"intermonthly-correlation": values.inter_monthly_correlation}, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) diff --git a/antarest/study/business/areas/properties_management.py b/antarest/study/business/areas/properties_management.py index 28a33087fd..654d589da5 100644 --- a/antarest/study/business/areas/properties_management.py +++ b/antarest/study/business/areas/properties_management.py @@ -18,11 +18,12 @@ from antarest.core.exceptions import ChildNotFoundError from antarest.study.business.all_optional_meta import all_optional_model -from antarest.study.business.utils import FieldInfo, FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import STUDY_VERSION_8_3, Study +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import FieldInfo, FormFieldsBaseModel +from antarest.study.model import STUDY_VERSION_8_3 from antarest.study.storage.rawstudy.model.filesystem.config.area import AdequacyPatchMode -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext AREA_PATH = "input/areas/{area}" THERMAL_PATH = "input/thermal/areas/{field}/{{area}}" @@ -129,16 +130,16 @@ def validation(cls, values: Dict[str, Any]) -> Dict[str, Any]: class PropertiesManager: - def __init__(self, storage_service: StudyStorageService): - self.storage_service = storage_service + def __init__(self, command_context: CommandContext): + self._command_context = command_context def get_field_values( self, - study: Study, + study: StudyInterface, area_id: str, ) -> PropertiesFormFields: - file_study = self.storage_service.get_storage(study).get_raw(study) - study_ver = file_study.config.version + file_study = study.get_files() + study_ver = study.version def get_value(field_info: FieldInfo) -> Any: start_ver = cast(int, field_info.get("start_version", 0)) @@ -159,13 +160,12 @@ def get_value(field_info: FieldInfo) -> Any: def set_field_values( self, - study: Study, + study: StudyInterface, area_id: str, field_values: PropertiesFormFields, ) -> None: commands: List[UpdateConfig] = [] - file_study = self.storage_service.get_storage(study).get_raw(study) - context = self.storage_service.variant_study_service.command_factory.command_context + file_study = study.get_files() for field_name, value in field_values.__iter__(): if value is not None: @@ -183,9 +183,12 @@ def set_field_values( commands.append( UpdateConfig( - target=target, data=data, command_context=context, study_version=file_study.config.version + target=target, + data=data, + command_context=self._command_context, + study_version=study.version, ) ) if commands: - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) diff --git a/antarest/study/business/areas/renewable_management.py b/antarest/study/business/areas/renewable_management.py index 15346f0820..af0e542564 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -11,32 +11,29 @@ # This file is part of the Antares project. import collections -from typing import Any, Dict, Mapping, MutableMapping, Optional, Sequence +from typing import Any, Dict, Mapping, MutableMapping, Sequence from antares.study.version import StudyVersion -from pydantic import field_validator from antarest.core.exceptions import DuplicateRenewableCluster, RenewableClusterConfigNotFound, RenewableClusterNotFound from antarest.core.model import JSON from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( RenewableConfig, - RenewableConfigType, RenewableProperties, RenewablePropertiesType, create_renewable_config, create_renewable_properties, ) -from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_renewables_cluster import CreateRenewablesCluster from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext _CLUSTER_PATH = "input/renewables/clusters/{area_id}/list/{cluster_id}" _CLUSTERS_PATH = "input/renewables/clusters/{area_id}/list" @@ -102,11 +99,11 @@ def json_schema_extra(schema: MutableMapping[str, Any]) -> None: def create_renewable_output( - study_version: str, + study_version: StudyVersion, cluster_id: str, config: Mapping[str, Any], ) -> "RenewableClusterOutput": - obj = create_renewable_config(study_version=StudyVersion.parse(study_version), **config, id=cluster_id) + obj = create_renewable_config(study_version=study_version, **config, id=cluster_id) kwargs = obj.model_dump(by_alias=False) return RenewableClusterOutput(**kwargs) @@ -114,21 +111,12 @@ def create_renewable_output( class RenewableManager: """ A manager class responsible for handling operations related to renewable clusters within a study. - - Attributes: - storage_service (StudyStorageService): A service responsible for study data storage and retrieval. """ - def __init__(self, storage_service: StudyStorageService): - self.storage_service = storage_service - - def _get_file_study(self, study: Study) -> FileStudy: - """ - Helper function to get raw study data. - """ - return self.storage_service.get_storage(study).get_raw(study) + def __init__(self, command_context: CommandContext): + self._command_context = command_context - def get_clusters(self, study: Study, area_id: str) -> Sequence[RenewableClusterOutput]: + def get_clusters(self, study: StudyInterface, area_id: str) -> Sequence[RenewableClusterOutput]: """ Fetches all clusters related to a specific area in a study. @@ -138,7 +126,7 @@ def get_clusters(self, study: Study, area_id: str) -> Sequence[RenewableClusterO Raises: RenewableClusterConfigNotFound: If the clusters configuration for the specified area is not found. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _CLUSTERS_PATH.format(area_id=area_id) try: @@ -150,7 +138,7 @@ def get_clusters(self, study: Study, area_id: str) -> Sequence[RenewableClusterO def get_all_renewables_props( self, - study: Study, + study: StudyInterface, ) -> Mapping[str, Mapping[str, RenewableClusterOutput]]: """ Retrieve all renewable clusters from all areas within a study. @@ -165,7 +153,7 @@ def get_all_renewables_props( RenewableClusterConfigNotFound: If no clusters are found in the specified area. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _ALL_CLUSTERS_PATH try: # may raise KeyError if the path is missing @@ -184,7 +172,7 @@ def get_all_renewables_props( return renewables_by_areas def create_cluster( - self, study: Study, area_id: str, cluster_data: RenewableClusterCreation + self, study: StudyInterface, area_id: str, cluster_data: RenewableClusterCreation ) -> RenewableClusterOutput: """ Creates a new cluster within an area in the study. @@ -197,15 +185,10 @@ def create_cluster( Returns: The newly created cluster. """ - file_study = self._get_file_study(study) - cluster = cluster_data.to_properties(StudyVersion.parse(study.version)) - command = self._make_create_cluster_cmd(area_id, cluster, file_study.config.version) - execute_or_add_commands( - study, - file_study, - [command], - self.storage_service, - ) + cluster = cluster_data.to_properties(study.version) + command = self._make_create_cluster_cmd(area_id, cluster, study.version) + + study.add_commands([command]) output = self.get_cluster(study, area_id, cluster.get_id()) return output @@ -215,12 +198,12 @@ def _make_create_cluster_cmd( command = CreateRenewablesCluster( area_id=area_id, parameters=cluster, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study_version, ) return command - def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> RenewableClusterOutput: + def get_cluster(self, study: StudyInterface, area_id: str, cluster_id: str) -> RenewableClusterOutput: """ Retrieves a single cluster's data for a specific area in a study. @@ -235,7 +218,7 @@ def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> RenewableC Raises: RenewableClusterNotFound: If the specified cluster is not found within the area. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) try: cluster = file_study.tree.get(path.split("/"), depth=1) @@ -245,7 +228,7 @@ def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> RenewableC def update_cluster( self, - study: Study, + study: StudyInterface, area_id: str, cluster_id: str, cluster_data: RenewableClusterInput, @@ -266,8 +249,7 @@ def update_cluster( RenewableClusterNotFound: If the cluster to update is not found. """ - study_version = StudyVersion.parse(study.version) - file_study = self._get_file_study(study) + file_study = study.get_files() path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) try: @@ -275,7 +257,7 @@ def update_cluster( except KeyError: raise RenewableClusterNotFound(path, cluster_id) from None else: - old_config = create_renewable_config(study_version, **values) + old_config = create_renewable_config(study.version, **values) new_values = cluster_data.model_dump(exclude_none=True) new_config = old_config.model_copy(update=new_values) @@ -289,19 +271,18 @@ def update_cluster( data[name] = new_data[name] # create the update config commands with the modified data - command_context = self.storage_service.variant_study_service.command_factory.command_context commands = [ UpdateConfig( - target=f"{path}/{key}", data=value, command_context=command_context, study_version=study_version + target=f"{path}/{key}", data=value, command_context=self._command_context, study_version=study.version ) for key, value in data.items() ] - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) values = new_config.model_dump(exclude={"id"}) return RenewableClusterOutput(**values, id=cluster_id) - def delete_clusters(self, study: Study, area_id: str, cluster_ids: Sequence[str]) -> None: + def delete_clusters(self, study: StudyInterface, area_id: str, cluster_ids: Sequence[str]) -> None: """ Deletes multiple clusters from an area in the study. @@ -310,24 +291,21 @@ def delete_clusters(self, study: Study, area_id: str, cluster_ids: Sequence[str] area_id: The identifier of the area where clusters will be deleted. cluster_ids: A sequence of cluster identifiers to be deleted. """ - file_study = self._get_file_study(study) - command_context = self.storage_service.variant_study_service.command_factory.command_context - commands = [ RemoveRenewablesCluster( area_id=area_id, cluster_id=cluster_id, - command_context=command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) for cluster_id in cluster_ids ] - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) def duplicate_cluster( self, - study: Study, + study: StudyInterface, area_id: str, source_id: str, new_cluster_name: str, @@ -353,12 +331,11 @@ def duplicate_cluster( raise DuplicateRenewableCluster(area_id, new_id) # Cluster duplication - study_version = StudyVersion.parse(study.version) current_cluster = self.get_cluster(study, area_id, source_id) current_cluster.name = new_cluster_name creation_form = RenewableClusterCreation(**current_cluster.model_dump(by_alias=False, exclude={"id"})) - new_config = creation_form.to_properties(study_version) - create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) + new_config = creation_form.to_properties(study.version) + create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study.version) # Matrix edition lower_source_id = source_id.lower() @@ -366,21 +343,19 @@ def duplicate_cluster( new_path = f"input/renewables/series/{area_id}/{lower_new_id}/series" # Prepare and execute commands - storage_service = self.storage_service.get_storage(study) - command_context = self.storage_service.variant_study_service.command_factory.command_context - current_matrix = storage_service.get(study, source_path)["data"] + file_study = study.get_files() + current_matrix = file_study.tree.get(source_path.split("/"))["data"] replace_matrix_cmd = ReplaceMatrix( - target=new_path, matrix=current_matrix, command_context=command_context, study_version=study_version + target=new_path, matrix=current_matrix, command_context=self._command_context, study_version=study.version ) commands = [create_cluster_cmd, replace_matrix_cmd] - - execute_or_add_commands(study, self._get_file_study(study), commands, self.storage_service) + study.add_commands(commands) return RenewableClusterOutput(**new_config.model_dump(by_alias=False)) def update_renewables_props( self, - study: Study, + study: StudyInterface, update_renewables_by_areas: Mapping[str, Mapping[str, RenewableClusterInput]], ) -> Mapping[str, Mapping[str, RenewableClusterOutput]]: old_renewables_by_areas = self.get_all_renewables_props(study) @@ -388,7 +363,7 @@ def update_renewables_props( # Prepare the commands to update the renewable clusters. commands = [] - study_version = StudyVersion.parse(study.version) + study_version = study.version for area_id, update_renewables_by_ids in update_renewables_by_areas.items(): old_renewables_by_ids = old_renewables_by_areas[area_id] for renewable_id, update_cluster in update_renewables_by_ids.items(): @@ -403,13 +378,12 @@ def update_renewables_props( cmd = UpdateConfig( target=path, data=properties.model_dump(mode="json", by_alias=True, exclude={"id"}), - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study_version, ) commands.append(cmd) - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) return new_renewables_by_areas diff --git a/antarest/study/business/areas/st_storage_management.py b/antarest/study/business/areas/st_storage_management.py index fa94ea046b..5fb93a0b68 100644 --- a/antarest/study/business/areas/st_storage_management.py +++ b/antarest/study/business/areas/st_storage_management.py @@ -31,8 +31,8 @@ from antarest.core.requests import CaseInsensitiveDict from antarest.core.serde import AntaresBaseModel from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import STUDY_VERSION_8_8, Study +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( STStorage880Config, @@ -43,11 +43,11 @@ create_st_storage_properties, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_st_storage import CreateSTStorage from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext @all_optional_model @@ -257,19 +257,12 @@ class STStorageManager: Manage short-term storage configuration in a study """ - def __init__(self, storage_service: StudyStorageService): - self.storage_service = storage_service - - def _get_file_study(self, study: Study) -> FileStudy: - """ - Helper function to get raw study data. - """ - - return self.storage_service.get_storage(study).get_raw(study) + def __init__(self, command_context: CommandContext): + self._command_context = command_context def create_storage( self, - study: Study, + study: StudyInterface, area_id: str, form: STStorageCreation, ) -> STStorageOutput: @@ -284,22 +277,17 @@ def create_storage( Returns: The ID of the newly created short-term storage. """ - file_study = self._get_file_study(study) + file_study = study.get_files() values_by_ids = _get_values_by_ids(file_study, area_id) - storage = form.to_properties(StudyVersion.parse(study.version)) + storage = form.to_properties(study.version) storage_id = storage.get_id() values = values_by_ids.get(storage_id) if values is not None: raise DuplicateSTStorage(area_id, storage_id) - command = self._make_create_cluster_cmd(area_id, storage, file_study.config.version) - execute_or_add_commands( - study, - file_study, - [command], - self.storage_service, - ) + command = self._make_create_cluster_cmd(area_id, storage, study.version) + study.add_commands([command]) output = self.get_storage(study, area_id, storage_id=storage_id) return output @@ -309,14 +297,14 @@ def _make_create_cluster_cmd( command = CreateSTStorage( area_id=area_id, parameters=cluster, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study_version, ) return command def get_storages( self, - study: Study, + study: StudyInterface, area_id: str, ) -> Sequence[STStorageOutput]: """ @@ -330,7 +318,7 @@ def get_storages( The list of forms used to display the short-term storages. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id="")[:-1] try: config = file_study.tree.get(path.split("/"), depth=3) @@ -341,13 +329,12 @@ def get_storages( # Sort STStorageConfig by groups and then by name order_by = operator.attrgetter("group", "name") - study_version = StudyVersion.parse(study.version) - storages = [create_storage_output(study_version, storage_id, options) for storage_id, options in config.items()] + storages = [create_storage_output(study.version, storage_id, options) for storage_id, options in config.items()] return sorted(storages, key=order_by) def get_all_storages_props( self, - study: Study, + study: StudyInterface, ) -> Mapping[str, Mapping[str, STStorageOutput]]: """ Retrieve all short-term storages from all areas within a study. @@ -362,7 +349,7 @@ def get_all_storages_props( STStorageConfigNotFound: If no storages are found in the specified area. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _ALL_STORAGE_PATH try: # may raise KeyError if the path is missing @@ -372,18 +359,17 @@ def get_all_storages_props( except KeyError: raise STStorageConfigNotFound(path) from None - study_version = StudyVersion.parse(study.version) storages_by_areas: MutableMapping[str, MutableMapping[str, STStorageOutput]] storages_by_areas = collections.defaultdict(dict) for area_id, cluster_obj in storages.items(): for cluster_id, cluster in cluster_obj.items(): - storages_by_areas[area_id][cluster_id] = create_storage_output(study_version, cluster_id, cluster) + storages_by_areas[area_id][cluster_id] = create_storage_output(study.version, cluster_id, cluster) return storages_by_areas def update_storages_props( self, - study: Study, + study: StudyInterface, update_storages_by_areas: Mapping[str, Mapping[str, STStorageInput]], ) -> Mapping[str, Mapping[str, STStorageOutput]]: old_storages_by_areas = self.get_all_storages_props(study) @@ -391,7 +377,7 @@ def update_storages_props( # Prepare the commands to update the storage clusters. commands = [] - study_version = StudyVersion.parse(study.version) + study_version = study.version for area_id, update_storages_by_ids in update_storages_by_areas.items(): old_storages_by_ids = old_storages_by_areas[area_id] for storage_id, update_cluster in update_storages_by_ids.items(): @@ -409,19 +395,17 @@ def update_storages_props( cmd = UpdateConfig( target=path, data=properties.model_dump(mode="json", by_alias=True, exclude={"id"}), - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study_version, ) commands.append(cmd) - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) - + study.add_commands(commands) return new_storages_by_areas def get_storage( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, ) -> STStorageOutput: @@ -437,17 +421,17 @@ def get_storage( Form used to display and edit a short-term storage. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) try: config = file_study.tree.get(path.split("/"), depth=1) except KeyError: raise STStorageNotFound(path, storage_id) from None - return create_storage_output(StudyVersion.parse(study.version), storage_id, config) + return create_storage_output(study.version, storage_id, config) def update_storage( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, form: STStorageInput, @@ -463,19 +447,14 @@ def update_storage( Returns: Updated form of short-term storage. """ - study_version = StudyVersion.parse(study.version) - - # For variants, this method requires generating a snapshot, which takes time. - # But sadly, there's no other way to prevent creating wrong commands. - - file_study = self._get_file_study(study) + file_study = study.get_files() values_by_ids = _get_values_by_ids(file_study, area_id) values = values_by_ids.get(storage_id) if values is None: path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) raise STStorageNotFound(path, storage_id) - old_config = create_st_storage_config(study_version, **values) + old_config = create_st_storage_config(study.version, **values) # use Python values to synchronize Config and Form values new_values = form.model_dump(mode="json", exclude_none=True) @@ -490,22 +469,21 @@ def update_storage( data[name] = new_data[name] # create the update config commands with the modified data - command_context = self.storage_service.variant_study_service.command_factory.command_context path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) commands = [ UpdateConfig( - target=f"{path}/{key}", data=value, command_context=command_context, study_version=study_version + target=f"{path}/{key}", data=value, command_context=self._command_context, study_version=study.version ) for key, value in data.items() ] - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) values = new_config.model_dump(mode="json", exclude={"id"}) return STStorageOutput(**values, id=storage_id) def delete_storages( self, - study: Study, + study: StudyInterface, area_id: str, storage_ids: Sequence[str], ) -> None: @@ -517,7 +495,7 @@ def delete_storages( area_id: The area ID of the short-term storage. storage_ids: IDs list of short-term storages to remove. """ - file_study = self._get_file_study(study) + file_study = study.get_files() values_by_ids = _get_values_by_ids(file_study, area_id) for storage_id in storage_ids: @@ -525,17 +503,21 @@ def delete_storages( path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) raise STStorageNotFound(path, storage_id) - command_context = self.storage_service.variant_study_service.command_factory.command_context + commands = [] for storage_id in storage_ids: - command = RemoveSTStorage( - area_id=area_id, - storage_id=storage_id, - command_context=command_context, - study_version=file_study.config.version, + commands.append( + RemoveSTStorage( + area_id=area_id, + storage_id=storage_id, + command_context=self._command_context, + study_version=study.version, + ) ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands(commands) - def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_cluster_name: str) -> STStorageOutput: + def duplicate_cluster( + self, study: StudyInterface, area_id: str, source_id: str, new_cluster_name: str + ) -> STStorageOutput: """ Creates a duplicate cluster within the study area with a new name. @@ -561,15 +543,14 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus current_cluster.name = new_cluster_name fields_to_exclude = {"id"} # We should remove the field 'enabled' for studies before v8.8 as it didn't exist - study_version = StudyVersion.parse(study.version) - if study_version < STUDY_VERSION_8_8: + if study.version < STUDY_VERSION_8_8: fields_to_exclude.add("enabled") creation_form = STStorageCreation.model_validate( current_cluster.model_dump(mode="json", by_alias=False, exclude=fields_to_exclude) ) - new_config = creation_form.to_properties(study_version) - create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) + new_config = creation_form.to_properties(study.version) + create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study.version) # Matrix edition lower_source_id = source_id.lower() @@ -585,23 +566,25 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus ] # Prepare and execute commands + file_study = study.get_files() commands: List[CreateSTStorage | ReplaceMatrix] = [create_cluster_cmd] - storage_service = self.storage_service.get_storage(study) - command_context = self.storage_service.variant_study_service.command_factory.command_context for source_path, new_path in zip(source_paths, new_paths): - current_matrix = storage_service.get(study, source_path)["data"] + current_matrix = file_study.tree.get(source_path.split("/"))["data"] command = ReplaceMatrix( - target=new_path, matrix=current_matrix, command_context=command_context, study_version=study_version + target=new_path, + matrix=current_matrix, + command_context=self._command_context, + study_version=study.version, ) commands.append(command) - execute_or_add_commands(study, self._get_file_study(study), commands, self.storage_service) + study.add_commands(commands) return STStorageOutput(**new_config.model_dump(mode="json", by_alias=False)) def get_matrix( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, ts_name: STStorageTimeSeries, @@ -623,12 +606,12 @@ def get_matrix( def _get_matrix_obj( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, ts_name: STStorageTimeSeries, ) -> MutableMapping[str, Any]: - file_study = self._get_file_study(study) + file_study = study.get_files() path = _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) try: matrix = file_study.tree.get(path.split("/"), depth=1) @@ -638,7 +621,7 @@ def _get_matrix_obj( def update_matrix( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, ts_name: STStorageTimeSeries, @@ -658,23 +641,24 @@ def update_matrix( def _save_matrix_obj( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, ts_name: STStorageTimeSeries, matrix_data: List[List[float]], ) -> None: - file_study = self._get_file_study(study) - command_context = self.storage_service.variant_study_service.command_factory.command_context path = _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) command = ReplaceMatrix( - target=path, matrix=matrix_data, command_context=command_context, study_version=file_study.config.version + target=path, + matrix=matrix_data, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def validate_matrices( self, - study: Study, + study: StudyInterface, area_id: str, storage_id: str, ) -> bool: diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 34849324d5..4509766f53 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -26,8 +26,8 @@ ) from antarest.core.model import JSON from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import STUDY_VERSION_8_7, Study +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import STUDY_VERSION_8_7 from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( Thermal870Config, @@ -36,8 +36,6 @@ create_thermal_config, create_thermal_properties, ) -from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_cluster import CreateCluster from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix @@ -50,6 +48,8 @@ "ThermalManager", ) +from antarest.study.storage.variantstudy.model.command_context import CommandContext + _CLUSTER_PATH = "input/thermal/clusters/{area_id}/list/{cluster_id}" _CLUSTERS_PATH = "input/thermal/clusters/{area_id}/list" _ALL_CLUSTERS_PATH = "input/thermal/clusters" @@ -136,24 +136,15 @@ class ThermalManager: Provides methods for creating, retrieving, updating, and deleting thermal clusters. Attributes: - storage_service: The service for accessing study storage. """ - def __init__(self, storage_service: StudyStorageService): + def __init__(self, command_context: CommandContext): """ Initializes an instance with the service for accessing study storage. """ + self._command_context = command_context - self.storage_service = storage_service - - def _get_file_study(self, study: Study) -> FileStudy: - """ - Helper function to get raw study data. - """ - - return self.storage_service.get_storage(study).get_raw(study) - - def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> ThermalClusterOutput: + def get_cluster(self, study: StudyInterface, area_id: str, cluster_id: str) -> ThermalClusterOutput: """ Get a cluster by ID. @@ -169,18 +160,17 @@ def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> ThermalClu ThermalClusterNotFound: If the specified cluster does not exist. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) try: cluster = file_study.tree.get(path.split("/"), depth=1) except KeyError: raise ThermalClusterNotFound(path, cluster_id) from None - study_version = StudyVersion.parse(study.version) - return create_thermal_output(study_version, cluster_id, cluster) + return create_thermal_output(study.version, cluster_id, cluster) def get_clusters( self, - study: Study, + study: StudyInterface, area_id: str, ) -> Sequence[ThermalClusterOutput]: """ @@ -197,18 +187,17 @@ def get_clusters( ThermalClusterConfigNotFound: If no clusters are found in the specified area. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _CLUSTERS_PATH.format(area_id=area_id) try: clusters = file_study.tree.get(path.split("/"), depth=3) except KeyError: raise ThermalClusterConfigNotFound(path, area_id) from None - study_version = StudyVersion.parse(study.version) - return [create_thermal_output(study_version, cluster_id, cluster) for cluster_id, cluster in clusters.items()] + return [create_thermal_output(study.version, cluster_id, cluster) for cluster_id, cluster in clusters.items()] def get_all_thermals_props( self, - study: Study, + study: StudyInterface, ) -> Mapping[str, Mapping[str, ThermalClusterOutput]]: """ Retrieve all thermal clusters from all areas within a study. @@ -223,7 +212,7 @@ def get_all_thermals_props( ThermalClusterConfigNotFound: If no clusters are found in the specified area. """ - file_study = self._get_file_study(study) + file_study = study.get_files() path = _ALL_CLUSTERS_PATH try: # may raise KeyError if the path is missing @@ -233,18 +222,17 @@ def get_all_thermals_props( except KeyError: raise ThermalClusterConfigNotFound(path) from None - study_version = StudyVersion.parse(study.version) thermals_by_areas: MutableMapping[str, MutableMapping[str, ThermalClusterOutput]] thermals_by_areas = collections.defaultdict(dict) for area_id, cluster_obj in clusters.items(): for cluster_id, cluster in cluster_obj.items(): - thermals_by_areas[area_id][cluster_id] = create_thermal_output(study_version, cluster_id, cluster) + thermals_by_areas[area_id][cluster_id] = create_thermal_output(study.version, cluster_id, cluster) return thermals_by_areas def update_thermals_props( self, - study: Study, + study: StudyInterface, update_thermals_by_areas: Mapping[str, Mapping[str, ThermalClusterInput]], ) -> Mapping[str, Mapping[str, ThermalClusterOutput]]: old_thermals_by_areas = self.get_all_thermals_props(study) @@ -252,7 +240,6 @@ def update_thermals_props( # Prepare the commands to update the thermal clusters. commands = [] - study_version = StudyVersion.parse(study.version) for area_id, update_thermals_by_ids in update_thermals_by_areas.items(): old_thermals_by_ids = old_thermals_by_areas[area_id] for thermal_id, update_cluster in update_thermals_by_ids.items(): @@ -263,28 +250,28 @@ def update_thermals_props( # Convert the DTO to a configuration object and update the configuration file. properties = create_thermal_config( - study_version, + study.version, **new_cluster.model_dump(mode="json", exclude_none=True), ) path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=thermal_id) cmd = UpdateConfig( target=path, data=properties.model_dump(mode="json", by_alias=True, exclude={"id"}), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=study_version, + command_context=self._command_context, + study_version=study.version, ) commands.append(cmd) - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) - + study.add_commands(commands) return new_thermals_by_areas @staticmethod def get_table_schema() -> JSON: return ThermalClusterOutput.model_json_schema() - def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalClusterCreation) -> ThermalClusterOutput: + def create_cluster( + self, study: StudyInterface, area_id: str, cluster_data: ThermalClusterCreation + ) -> ThermalClusterOutput: """ Create a new cluster. @@ -297,15 +284,10 @@ def create_cluster(self, study: Study, area_id: str, cluster_data: ThermalCluste The created cluster. """ - file_study = self._get_file_study(study) - cluster = cluster_data.to_properties(StudyVersion.parse(study.version)) - command = self._make_create_cluster_cmd(area_id, cluster, file_study.config.version) - execute_or_add_commands( - study, - file_study, - [command], - self.storage_service, - ) + cluster = cluster_data.to_properties(study.version) + command = self._make_create_cluster_cmd(area_id, cluster, study.version) + study.add_commands([command]) + output = self.get_cluster(study, area_id, cluster.get_id()) return output @@ -318,12 +300,12 @@ def _make_create_cluster_cmd( area_id=area_id, parameters=cluster, study_version=study_version, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, ) def update_cluster( self, - study: Study, + study: StudyInterface, area_id: str, cluster_id: str, cluster_data: ThermalClusterInput, @@ -346,15 +328,14 @@ def update_cluster( in the provided cluster_data. """ - study_version = StudyVersion.parse(study.version) - file_study = self._get_file_study(study) + file_study = study.get_files() path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) try: values = file_study.tree.get(path.split("/"), depth=1) except KeyError: raise ThermalClusterNotFound(path, cluster_id) from None else: - old_config = create_thermal_config(study_version, **values) + old_config = create_thermal_config(study.version, **values) # Use Python values to synchronize Config and Form values new_values = cluster_data.model_dump(mode="json", exclude_none=True) @@ -369,19 +350,18 @@ def update_cluster( data[name] = new_data[name] # create the update config commands with the modified data - command_context = self.storage_service.variant_study_service.command_factory.command_context commands = [ UpdateConfig( - target=f"{path}/{key}", data=value, command_context=command_context, study_version=study_version + target=f"{path}/{key}", data=value, command_context=self._command_context, study_version=study.version ) for key, value in data.items() ] - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) values = {**new_config.model_dump(mode="json", exclude={"id"})} return ThermalClusterOutput(**values, id=cluster_id) - def delete_clusters(self, study: Study, area_id: str, cluster_ids: Sequence[str]) -> None: + def delete_clusters(self, study: StudyInterface, area_id: str, cluster_ids: Sequence[str]) -> None: """ Delete the clusters with the given IDs in the given area of the given study. @@ -391,24 +371,21 @@ def delete_clusters(self, study: Study, area_id: str, cluster_ids: Sequence[str] cluster_ids: The IDs of the clusters to delete. """ - file_study = self._get_file_study(study) - command_context = self.storage_service.variant_study_service.command_factory.command_context - commands = [ RemoveCluster( area_id=area_id, cluster_id=cluster_id, - command_context=command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) for cluster_id in cluster_ids ] - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) def duplicate_cluster( self, - study: Study, + study: StudyInterface, area_id: str, source_id: str, new_cluster_name: str, @@ -433,11 +410,13 @@ def duplicate_cluster( if any(lower_new_id == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): raise DuplicateThermalCluster(area_id, new_id) + file_study = study.get_files() + study_version = study.version + # Cluster duplication source_cluster = self.get_cluster(study, area_id, source_id) source_cluster.name = new_cluster_name creation_form = ThermalClusterCreation(**source_cluster.model_dump(mode="json", by_alias=False, exclude={"id"})) - study_version = StudyVersion.parse(study.version) new_config = creation_form.to_properties(study_version) create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) @@ -453,7 +432,6 @@ def duplicate_cluster( f"input/thermal/prepro/{area_id}/{lower_new_id}/modulation", f"input/thermal/prepro/{area_id}/{lower_new_id}/data", ] - study_version = StudyVersion.parse(study.version) if study_version >= STUDY_VERSION_8_7: source_paths.append(f"input/thermal/series/{area_id}/{lower_source_id}/CO2Cost") source_paths.append(f"input/thermal/series/{area_id}/{lower_source_id}/fuelCost") @@ -462,30 +440,31 @@ def duplicate_cluster( # Prepare and execute commands commands: List[CreateCluster | ReplaceMatrix] = [create_cluster_cmd] - storage_service = self.storage_service.get_storage(study) - command_context = self.storage_service.variant_study_service.command_factory.command_context + command_context = self._command_context for source_path, new_path in zip(source_paths, new_paths): - current_matrix = storage_service.get(study, source_path)["data"] + current_matrix = file_study.tree.get(source_path.split("/"))["data"] command = ReplaceMatrix( target=new_path, matrix=current_matrix, command_context=command_context, study_version=study_version ) commands.append(command) - execute_or_add_commands(study, self._get_file_study(study), commands, self.storage_service) + study.add_commands(commands) return ThermalClusterOutput(**new_config.model_dump(mode="json", by_alias=False)) - def validate_series(self, study: Study, area_id: str, cluster_id: str) -> bool: + def validate_series(self, study: StudyInterface, area_id: str, cluster_id: str) -> bool: lower_cluster_id = cluster_id.lower() thermal_cluster_path = Path(f"input/thermal/series/{area_id}/{lower_cluster_id}") series_path = [thermal_cluster_path / "series"] - if StudyVersion.parse(study.version) >= STUDY_VERSION_8_7: + + file_study = study.get_files() + if study.version >= STUDY_VERSION_8_7: series_path.append(thermal_cluster_path / "CO2Cost") series_path.append(thermal_cluster_path / "fuelCost") ts_widths: MutableMapping[int, MutableSequence[str]] = {} for ts_path in series_path: - matrix = self.storage_service.get_storage(study).get(study, ts_path.as_posix()) + matrix = file_study.tree.get(ts_path.as_posix().split("/")) matrix_data = matrix["data"] matrix_height = len(matrix_data) # We ignore empty matrices as there are default matrices for the simulator. diff --git a/antarest/study/business/binding_constraint_management.py b/antarest/study/business/binding_constraint_management.py index e4471b3e75..c208257561 100644 --- a/antarest/study/business/binding_constraint_management.py +++ b/antarest/study/business/binding_constraint_management.py @@ -35,8 +35,8 @@ from antarest.core.serde import AntaresBaseModel from antarest.core.utils.string import to_camel_case from antarest.study.business.all_optional_meta import camel_case_model -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import STUDY_VERSION_8_3, STUDY_VERSION_8_7, Study +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import STUDY_VERSION_8_3, STUDY_VERSION_8_7 from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import ( DEFAULT_GROUP, DEFAULT_OPERATOR, @@ -46,7 +46,6 @@ ) from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.business.matrix_constants.binding_constraint.series_after_v87 import ( default_bc_hourly as default_bc_hourly_87, ) @@ -80,7 +79,6 @@ ) from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy logger = logging.getLogger(__name__) @@ -474,9 +472,9 @@ def _validate_binding_constraints(file_study: FileStudy, bcs: Sequence[Constrain class BindingConstraintManager: def __init__( self, - storage_service: StudyStorageService, + command_context: CommandContext, ) -> None: - self.storage_service = storage_service + self._command_context = command_context @staticmethod def parse_and_add_terms(key: str, value: Any, adapted_constraint: ConstraintOutput) -> None: @@ -592,9 +590,8 @@ def terms_to_coeffs(terms: Sequence[ConstraintTerm]) -> Dict[str, List[float]]: coeffs[term.id].append(term.offset) return coeffs - def check_binding_constraints_exists(self, study: Study, bc_ids: List[str]) -> None: - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + def check_binding_constraints_exists(self, study: StudyInterface, bc_ids: List[str]) -> None: + file_study = study.get_files() existing_constraints = file_study.tree.get(["input", "bindingconstraints", "bindingconstraints"]) existing_ids = {constraint["id"] for constraint in existing_constraints.values()} @@ -604,7 +601,7 @@ def check_binding_constraints_exists(self, study: Study, bc_ids: List[str]) -> N if missing_bc_ids: raise BindingConstraintNotFound(f"Binding constraint(s) '{missing_bc_ids}' not found") - def get_binding_constraint(self, study: Study, bc_id: str) -> ConstraintOutput: + def get_binding_constraint(self, study: StudyInterface, bc_id: str) -> ConstraintOutput: """ Retrieves a binding constraint by its ID within a given study. @@ -618,14 +615,13 @@ def get_binding_constraint(self, study: Study, bc_id: str) -> ConstraintOutput: Raises: BindingConstraintNotFound: If no binding constraint with the specified ID is found. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + file_study = study.get_files() config = file_study.tree.get(["input", "bindingconstraints", "bindingconstraints"]) constraints_by_id: Dict[str, ConstraintOutput] = CaseInsensitiveDict() # type: ignore for constraint in config.values(): - constraint_config = self.constraint_model_adapter(constraint, StudyVersion.parse(study.version)) + constraint_config = self.constraint_model_adapter(constraint, study.version) constraints_by_id[constraint_config.id] = constraint_config if bc_id not in constraints_by_id: @@ -634,7 +630,7 @@ def get_binding_constraint(self, study: Study, bc_id: str) -> ConstraintOutput: return constraints_by_id[bc_id] def get_binding_constraints( - self, study: Study, filters: ConstraintFilters = ConstraintFilters() + self, study: StudyInterface, filters: ConstraintFilters = ConstraintFilters() ) -> Sequence[ConstraintOutput]: """ Retrieves all binding constraints within a given study, optionally filtered by specific criteria. @@ -646,14 +642,13 @@ def get_binding_constraints( Returns: A list of ConstraintOutput objects representing the binding constraints that match the specified filters. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + file_study = study.get_files() config = file_study.tree.get(["input", "bindingconstraints", "bindingconstraints"]) - outputs = [self.constraint_model_adapter(c, StudyVersion.parse(study.version)) for c in config.values()] + outputs = [self.constraint_model_adapter(c, study.version) for c in config.values()] filtered_constraints = list(filter(lambda c: filters.match_filters(c), outputs)) return filtered_constraints - def get_grouped_constraints(self, study: Study) -> Mapping[str, Sequence[ConstraintOutput]]: + def get_grouped_constraints(self, study: StudyInterface) -> Mapping[str, Sequence[ConstraintOutput]]: """ Retrieves and groups all binding constraints by their group names within a given study. @@ -670,19 +665,18 @@ def get_grouped_constraints(self, study: Study) -> Mapping[str, Sequence[Constra The grouping considers the exact group name, implying case sensitivity. If case-insensitive grouping is required, normalization of group names to a uniform case (e.g., all lower or upper) should be performed. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + file_study = study.get_files() config = file_study.tree.get(["input", "bindingconstraints", "bindingconstraints"]) grouped_constraints = CaseInsensitiveDict() for constraint in config.values(): - constraint_config = self.constraint_model_adapter(constraint, StudyVersion.parse(study.version)) + constraint_config = self.constraint_model_adapter(constraint, study.version) constraint_group = getattr(constraint_config, "group", DEFAULT_GROUP) grouped_constraints.setdefault(constraint_group, []).append(constraint_config) return grouped_constraints - def get_constraints_by_group(self, study: Study, group_name: str) -> Sequence[ConstraintOutput]: + def get_constraints_by_group(self, study: StudyInterface, group_name: str) -> Sequence[ConstraintOutput]: """ Retrieve all binding constraints belonging to a specified group within a study. @@ -703,7 +697,7 @@ def get_constraints_by_group(self, study: Study, group_name: str) -> Sequence[Co return grouped_constraints[group_name] - def validate_constraint_group(self, study: Study, group_name: str) -> bool: + def validate_constraint_group(self, study: StudyInterface, group_name: str) -> bool: """ Validates if the specified group name exists within the study's binding constraints and checks the validity of the constraints within that group. @@ -722,8 +716,7 @@ def validate_constraint_group(self, study: Study, group_name: str) -> bool: Raises: BindingConstraintNotFound: If no matching group name is found in a case-insensitive manner. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + file_study = study.get_files() grouped_constraints = self.get_grouped_constraints(study) if group_name not in grouped_constraints: @@ -732,7 +725,7 @@ def validate_constraint_group(self, study: Study, group_name: str) -> bool: constraints = grouped_constraints[group_name] return _validate_binding_constraints(file_study, constraints) - def validate_constraint_groups(self, study: Study) -> bool: + def validate_constraint_groups(self, study: StudyInterface) -> bool: """ Validates all groups of binding constraints within the given study. @@ -749,8 +742,7 @@ def validate_constraint_groups(self, study: Study) -> bool: Raises: IncoherenceBetweenMatricesLength: If any validation checks fail. """ - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + file_study = study.get_files() grouped_constraints = self.get_grouped_constraints(study) invalid_groups = {} @@ -768,11 +760,10 @@ def validate_constraint_groups(self, study: Study) -> bool: def create_binding_constraint( self, - study: Study, + study: StudyInterface, data: ConstraintCreation, ) -> ConstraintOutput: bc_id = transform_name_to_id(data.name) - version = StudyVersion.parse(study.version) if not bc_id: raise InvalidConstraintName(f"Invalid binding constraint name: {data.name}.") @@ -780,7 +771,7 @@ def create_binding_constraint( if bc_id in {bc.id for bc in self.get_binding_constraints(study)}: raise DuplicateConstraintName(f"A binding constraint with the same name already exists: {bc_id}.") - check_attributes_coherence(data, version, data.operator or DEFAULT_OPERATOR) + check_attributes_coherence(data, study.version, data.operator or DEFAULT_OPERATOR) new_constraint = { "name": data.name, @@ -788,8 +779,8 @@ def create_binding_constraint( } args = { **new_constraint, - "command_context": self.storage_service.variant_study_service.command_factory.command_context, - "study_version": version, + "command_context": self._command_context, + "study_version": study.version, } if data.terms: args["coeffs"] = self.terms_to_coeffs(data.terms) @@ -797,20 +788,20 @@ def create_binding_constraint( command = CreateBindingConstraint(**args) # Validates the matrices. Needed when the study is a variant because we only append the command to the list - if isinstance(study, VariantStudy): - time_step = data.time_step or DEFAULT_TIMESTEP - command.validates_and_fills_matrices( - time_step=time_step, specific_matrices=None, version=version, create=True - ) + time_step = data.time_step or DEFAULT_TIMESTEP + command.validates_and_fills_matrices( + time_step=time_step, specific_matrices=None, version=study.version, create=True + ) - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) # Processes the constraints to add them inside the endpoint response. new_constraint["id"] = bc_id - return self.constraint_model_adapter(new_constraint, version) + return self.constraint_model_adapter(new_constraint, study.version) - def duplicate_binding_constraint(self, study: Study, source_id: str, new_constraint_name: str) -> ConstraintOutput: + def duplicate_binding_constraint( + self, study: StudyInterface, source_id: str, new_constraint_name: str + ) -> ConstraintOutput: """ Creates a duplicate constraint with a new name. @@ -845,15 +836,15 @@ def duplicate_binding_constraint(self, study: Study, source_id: str, new_constra } args = { **new_constraint, - "command_context": self.storage_service.variant_study_service.command_factory.command_context, - "study_version": StudyVersion.parse(study.version), + "command_context": self._command_context, + "study_version": study.version, } if source_constraint.terms: args["coeffs"] = self.terms_to_coeffs(source_constraint.terms) # Retrieval of the source constraint matrices - file_study = self.storage_service.get_storage(study).get_raw(study) - if file_study.config.version < STUDY_VERSION_8_7: + file_study = study.get_files() + if study.version < STUDY_VERSION_8_7: matrix = file_study.tree.get(["input", "bindingconstraints", source_id]) args["values"] = matrix["data"] else: @@ -872,7 +863,7 @@ def duplicate_binding_constraint(self, study: Study, source_id: str, new_constra # Creates and applies constraint command = CreateBindingConstraint(**args) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) # Returns the new constraint source_constraint.name = new_constraint_name @@ -881,16 +872,14 @@ def duplicate_binding_constraint(self, study: Study, source_id: str, new_constra def update_binding_constraint( self, - study: Study, + study: StudyInterface, binding_constraint_id: str, data: ConstraintInput, existing_constraint: Optional[ConstraintOutput] = None, ) -> ConstraintOutput: - file_study = self.storage_service.get_storage(study).get_raw(study) existing_constraint = existing_constraint or self.get_binding_constraint(study, binding_constraint_id) - study_version = StudyVersion.parse(study.version) - check_attributes_coherence(data, study_version, data.operator or existing_constraint.operator) + check_attributes_coherence(data, study.version, data.operator or existing_constraint.operator) upd_constraint = { "id": binding_constraint_id, @@ -898,46 +887,46 @@ def update_binding_constraint( } args = { **upd_constraint, - "command_context": self.storage_service.variant_study_service.command_factory.command_context, - "study_version": study_version, + "command_context": self._command_context, + "study_version": study.version, } if data.terms: args["coeffs"] = self.terms_to_coeffs(data.terms) if data.time_step is not None and data.time_step != existing_constraint.time_step: # The user changed the time step, we need to update the matrix accordingly - args = _replace_matrices_according_to_frequency_and_version(data, study_version, args) + args = _replace_matrices_according_to_frequency_and_version(data, study.version, args) command = UpdateBindingConstraint(**args) # Validates the matrices. Needed when the study is a variant because we only append the command to the list - if isinstance(study, VariantStudy): - updated_matrices = [term for term in [m.value for m in TermMatrices] if getattr(data, term)] - if updated_matrices: - time_step = data.time_step or existing_constraint.time_step - command.validates_and_fills_matrices( - time_step=time_step, specific_matrices=updated_matrices, version=study_version, create=False # type: ignore - ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + updated_matrices = [term for term in [m.value for m in TermMatrices] if getattr(data, term)] + if updated_matrices: + time_step = data.time_step or existing_constraint.time_step + command.validates_and_fills_matrices( + time_step=time_step, specific_matrices=updated_matrices, version=study.version, create=False # type: ignore + ) + + study.add_commands([command]) # Constructs the endpoint response. upd_constraint["name"] = existing_constraint.name upd_constraint["type"] = upd_constraint.get("time_step", existing_constraint.time_step) upd_constraint["terms"] = data.terms or existing_constraint.terms new_fields = ["enabled", "operator", "comments", "terms"] - if study_version >= STUDY_VERSION_8_3: + if study.version >= STUDY_VERSION_8_3: new_fields.extend(["filter_year_by_year", "filter_synthesis"]) - if study_version >= STUDY_VERSION_8_7: + if study.version >= STUDY_VERSION_8_7: new_fields.append("group") for field in new_fields: if field not in upd_constraint: upd_constraint[field] = getattr(data, field) or getattr(existing_constraint, field) - return self.constraint_model_adapter(upd_constraint, study_version) + return self.constraint_model_adapter(upd_constraint, study.version) def update_binding_constraints( self, - study: Study, + study: StudyInterface, bcs_by_ids: Mapping[str, ConstraintInput], ) -> Mapping[str, ConstraintOutput]: """ @@ -962,7 +951,7 @@ def update_binding_constraints( # Variant study with less than 50 updated constraints updated_constraints = {} - if len(bcs_by_ids) < 50 and isinstance(study, VariantStudy): + if len(bcs_by_ids) < 50: existing_constraints = {bc.id: bc for bc in self.get_binding_constraints(study)} for bc_id, data in bcs_by_ids.items(): updated_constraints[bc_id] = self.update_binding_constraint( @@ -971,33 +960,31 @@ def update_binding_constraints( return updated_constraints # More efficient way of doing things but using less readable commands. - study_version = StudyVersion.parse(study.version) commands = [] - command_context = self.storage_service.variant_study_service.command_factory.command_context - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() config = file_study.tree.get(["input", "bindingconstraints", "bindingconstraints"]) dict_config = {value["id"]: key for (key, value) in config.items()} for bc_id, value in bcs_by_ids.items(): if bc_id not in dict_config: raise BindingConstraintNotFound(f"Binding constraint '{bc_id}' not found") - props = create_binding_constraint_config(study_version, **value.model_dump()) + props = create_binding_constraint_config(study.version, **value.model_dump()) new_values = props.model_dump(mode="json", by_alias=True, exclude_unset=True) upd_obj = config[dict_config[bc_id]] current_value = copy.deepcopy(upd_obj) upd_obj.update(new_values) - output = self.constraint_model_adapter(upd_obj, study_version) + output = self.constraint_model_adapter(upd_obj, study.version) updated_constraints[bc_id] = output if value.time_step and value.time_step != BindingConstraintFrequency(current_value["type"]): # The user changed the time step, we need to update the matrix accordingly replace_matrix_commands = _generate_replace_matrix_commands( - bc_id, study_version, value, output.operator, command_context + bc_id, study.version, value, output.operator, self._command_context ) commands.extend(replace_matrix_commands) - if value.operator and study_version >= STUDY_VERSION_8_7: + if value.operator and study.version >= STUDY_VERSION_8_7: # The user changed the operator, we have to rename matrices accordingly existing_operator = BindingConstraintOperator(current_value["operator"]) update_matrices_names(file_study, bc_id, existing_operator, value.operator) @@ -1006,14 +993,14 @@ def update_binding_constraints( command = UpdateConfig( target="input/bindingconstraints/bindingconstraints", data=config, - command_context=command_context, - study_version=study_version, + command_context=self._command_context, + study_version=study.version, ) commands.append(command) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) return updated_constraints - def remove_binding_constraint(self, study: Study, binding_constraint_id: str) -> None: + def remove_binding_constraint(self, study: StudyInterface, binding_constraint_id: str) -> None: """ Removes a binding constraint from a study. @@ -1026,14 +1013,10 @@ def remove_binding_constraint(self, study: Study, binding_constraint_id: str) -> """ # Check the existence of the binding constraint before removing it bc = self.get_binding_constraint(study, binding_constraint_id) - command_context = self.storage_service.variant_study_service.command_factory.command_context - file_study = self.storage_service.get_storage(study).get_raw(study) - command = RemoveBindingConstraint( - id=bc.id, command_context=command_context, study_version=file_study.config.version - ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + command = RemoveBindingConstraint(id=bc.id, command_context=self._command_context, study_version=study.version) + study.add_commands([command]) - def remove_multiple_binding_constraints(self, study: Study, binding_constraints_ids: List[str]) -> None: + def remove_multiple_binding_constraints(self, study: StudyInterface, binding_constraints_ids: List[str]) -> None: """ Removes multiple binding constraints from a study. @@ -1047,37 +1030,32 @@ def remove_multiple_binding_constraints(self, study: Study, binding_constraints_ self.check_binding_constraints_exists(study, binding_constraints_ids) - command_context = self.storage_service.variant_study_service.command_factory.command_context - file_study = self.storage_service.get_storage(study).get_raw(study) - command = RemoveMultipleBindingConstraints( ids=binding_constraints_ids, - command_context=command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def _update_constraint_with_terms( - self, study: Study, bc: ConstraintOutput, terms: Mapping[str, ConstraintTerm] + self, study: StudyInterface, bc: ConstraintOutput, terms: Mapping[str, ConstraintTerm] ) -> None: coeffs = { term_id: [term.weight, term.offset] if term.offset else [term.weight] for term_id, term in terms.items() } - command_context = self.storage_service.variant_study_service.command_factory.command_context - file_study = self.storage_service.get_storage(study).get_raw(study) args = { "id": bc.id, "coeffs": coeffs, - "command_context": command_context, - "study_version": file_study.config.version, + "command_context": self._command_context, + "study_version": study.version, } command = UpdateBindingConstraint.model_validate(args) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def update_constraint_terms( self, - study: Study, + study: StudyInterface, binding_constraint_id: str, constraint_terms: Sequence[ConstraintTerm], update_mode: str = "replace", @@ -1115,7 +1093,7 @@ def update_constraint_terms( self._update_constraint_with_terms(study, constraint, existing_terms) def create_constraint_terms( - self, study: Study, binding_constraint_id: str, constraint_terms: Sequence[ConstraintTerm] + self, study: StudyInterface, binding_constraint_id: str, constraint_terms: Sequence[ConstraintTerm] ) -> None: """ Adds new constraint terms to an existing binding constraint. @@ -1129,7 +1107,7 @@ def create_constraint_terms( def remove_constraint_term( self, - study: Study, + study: StudyInterface, binding_constraint_id: str, term_id: str, ) -> None: diff --git a/antarest/study/business/config_management.py b/antarest/study/business/config_management.py index 872336cabd..a203dca20d 100644 --- a/antarest/study/business/config_management.py +++ b/antarest/study/business/config_management.py @@ -12,40 +12,36 @@ from typing import Dict, List, Optional -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.helpers import FileStudyHelpers -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.update_playlist import UpdatePlaylist +from antarest.study.storage.variantstudy.model.command_context import CommandContext class ConfigManager: def __init__( self, - storage_service: StudyStorageService, + command_context: CommandContext, ) -> None: - self.storage_service = storage_service + self._command_context = command_context - def get_playlist(self, study: Study) -> Optional[Dict[int, float]]: - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) - return FileStudyHelpers.get_playlist(file_study) + def get_playlist(self, study: StudyInterface) -> Optional[Dict[int, float]]: + return FileStudyHelpers.get_playlist(study.get_files()) def set_playlist( self, - study: Study, + study: StudyInterface, playlist: Optional[List[int]], weights: Optional[Dict[int, int]], reverse: bool, active: bool, ) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) command = UpdatePlaylist( items=playlist, weights=weights, reverse=reverse, active=active, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) diff --git a/antarest/study/business/correlation_management.py b/antarest/study/business/correlation_management.py index cc8ac58853..489073d783 100644 --- a/antarest/study/business/correlation_management.py +++ b/antarest/study/business/correlation_management.py @@ -23,11 +23,11 @@ from antarest.core.exceptions import AreaNotFound from antarest.study.business.model.area_model import AreaInfoDTO -from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import FormFieldsBaseModel from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class AreaCoefficientItem(FormFieldsBaseModel): @@ -200,8 +200,8 @@ class CorrelationManager: # categories but the usage is deprecated. url = ["input", "hydro", "prepro", "correlation", "annual"] - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context def _get_array( self, @@ -213,23 +213,21 @@ def _get_array( def _set_array( self, - study: Study, - file_study: FileStudy, + study: StudyInterface, area_ids: Sequence[str], array: npt.NDArray[np.float64], ) -> None: correlation_cfg = _array_to_config(area_ids, array) - command_context = self.storage_service.variant_study_service.command_factory.command_context command = UpdateConfig( target="/".join(self.url), data=correlation_cfg, - command_context=command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def get_correlation_form_fields( - self, all_areas: List[AreaInfoDTO], study: Study, area_id: str + self, all_areas: List[AreaInfoDTO], study: StudyInterface, area_id: str ) -> CorrelationFormFields: """ Get the correlation form fields (percentage values) for a given area. @@ -242,7 +240,7 @@ def get_correlation_form_fields( Returns: The correlation coefficients. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() area_ids = [area.id for area in all_areas] array = self._get_array(file_study, area_ids) @@ -265,7 +263,7 @@ def get_correlation_form_fields( def set_correlation_form_fields( self, all_areas: List[AreaInfoDTO], - study: Study, + study: StudyInterface, area_id: str, data: CorrelationFormFields, ) -> CorrelationFormFields: @@ -292,13 +290,13 @@ def set_correlation_form_fields( # sort for deterministic error message and testing raise AreaNotFound(*sorted(invalid_ids)) - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() array = self._get_array(file_study, area_ids) j = area_ids.index(area_id) for i, coefficient in enumerate(correlation_values.values()): array[i][j] = coefficient / 100 array[j][i] = coefficient / 100 - self._set_array(study, file_study, area_ids, array) + self._set_array(study, area_ids, array) column = array[:, area_ids.index(area_id)] * 100 return CorrelationFormFields.model_construct( @@ -308,7 +306,7 @@ def set_correlation_form_fields( ) def get_correlation_matrix( - self, all_areas: List[AreaInfoDTO], study: Study, columns: List[str] + self, all_areas: List[AreaInfoDTO], study: StudyInterface, columns: List[str] ) -> CorrelationMatrix: """ Read the correlation coefficients and get the correlation matrix (values in the range -1 to 1). @@ -321,7 +319,7 @@ def get_correlation_matrix( Returns: The correlation matrix. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() area_ids = [area.id for area in all_areas] columns = [a for a in area_ids if a in columns] if columns else area_ids array = self._get_array(file_study, area_ids) @@ -333,7 +331,7 @@ def get_correlation_matrix( def set_correlation_matrix( self, all_areas: List[AreaInfoDTO], - study: Study, + study: StudyInterface, matrix: CorrelationMatrix, ) -> CorrelationMatrix: """ @@ -347,7 +345,7 @@ def set_correlation_matrix( Returns: The updated correlation matrix. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() area_ids = [area.id for area in all_areas] array = self._get_array(file_study, area_ids) @@ -361,7 +359,7 @@ def set_correlation_matrix( array[i][j] = coefficient array[j][i] = coefficient - self._set_array(study, file_study, area_ids, array) + self._set_array(study, area_ids, array) # noinspection PyTypeChecker data = [[c for i, c in enumerate(row) if area_ids[i] in matrix.columns] for row in array.tolist()] diff --git a/antarest/study/business/district_manager.py b/antarest/study/business/district_manager.py index 773a7e6056..92086d7c77 100644 --- a/antarest/study/business/district_manager.py +++ b/antarest/study/business/district_manager.py @@ -14,13 +14,12 @@ from antarest.core.exceptions import AreaNotFound, DistrictAlreadyExist, DistrictNotFound from antarest.core.serde import AntaresBaseModel -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.filesystem.config.identifier import transform_name_to_id -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_district import CreateDistrict, DistrictBaseFilter from antarest.study.storage.variantstudy.model.command.remove_district import RemoveDistrict from antarest.study.storage.variantstudy.model.command.update_district import UpdateDistrict +from antarest.study.storage.variantstudy.model.command_context import CommandContext class DistrictUpdateDTO(AntaresBaseModel): @@ -53,10 +52,10 @@ class DistrictManager: This class updates the `input/areas/sets.ini` file of the study working directory. """ - def __init__(self, storage_service: StudyStorageService): - self.storage_service = storage_service + def __init__(self, command_context: CommandContext): + self._command_context = command_context - def get_districts(self, study: Study) -> List[DistrictInfoDTO]: + def get_districts(self, study: StudyInterface) -> List[DistrictInfoDTO]: """ Get the list of districts defined in this study. @@ -66,7 +65,7 @@ def get_districts(self, study: Study) -> List[DistrictInfoDTO]: Returns: The (unordered) list of Data Transfer Objects (DTO) representing districts. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() all_areas = list(file_study.config.areas) districts = [] for district_id, district in file_study.config.sets.items(): @@ -84,7 +83,7 @@ def get_districts(self, study: Study) -> List[DistrictInfoDTO]: def create_district( self, - study: Study, + study: StudyInterface, dto: DistrictCreationDTO, ) -> DistrictInfoDTO: """ @@ -101,7 +100,7 @@ def create_district( DistrictAlreadyExist: exception raised when district already exists (duplicate). AreaNotFound: exception raised when one (or more) area(s) don't exist in the study. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() district_id = transform_name_to_id(dto.name) if district_id in file_study.config.sets: raise DistrictAlreadyExist(district_id) @@ -115,10 +114,10 @@ def create_district( comments=dto.comments, base_filter=DistrictBaseFilter.remove_all, filter_items=list(areas), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) return DistrictInfoDTO( id=district_id, name=dto.name, @@ -129,7 +128,7 @@ def create_district( def update_district( self, - study: Study, + study: StudyInterface, district_id: str, dto: DistrictUpdateDTO, ) -> None: @@ -148,7 +147,7 @@ def update_district( DistrictNotFound: exception raised when district is not found in the study. AreaNotFound: exception raised when one (or more) area(s) don't exist in the study. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() if district_id not in file_study.config.sets: raise DistrictNotFound(district_id) areas = set(dto.areas or []) @@ -161,14 +160,14 @@ def update_district( filter_items=dto.areas or [], output=dto.output, comments=dto.comments, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def remove_district( self, - study: Study, + study: StudyInterface, district_id: str, ) -> None: """ @@ -181,12 +180,12 @@ def remove_district( Raises: DistrictNotFound: exception raised when district is not found in the study. """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() if district_id not in file_study.config.sets: raise DistrictNotFound(district_id) command = RemoveDistrict( id=district_id, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) diff --git a/antarest/study/business/general_management.py b/antarest/study/business/general_management.py index 0fbca596e7..336e5fd4ae 100644 --- a/antarest/study/business/general_management.py +++ b/antarest/study/business/general_management.py @@ -16,10 +16,9 @@ from antarest.study.business.all_optional_meta import all_optional_model from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import STUDY_VERSION_7_1, STUDY_VERSION_8, Study -from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel +from antarest.study.model import STUDY_VERSION_7_1, STUDY_VERSION_8 from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command_context import CommandContext @@ -224,14 +223,14 @@ def day_fields_validation(cls, values: Union[Dict[str, Any], ValidationInfo]) -> class GeneralManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_field_values(self, study: Study) -> GeneralFormFields: + def get_field_values(self, study: StudyInterface) -> GeneralFormFields: """ Get General field values for the webapp form """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() general_data = file_study.tree.get(GENERAL_DATA_PATH.split("/")) general = general_data.get(GENERAL, {}) output = general_data.get(OUTPUT, {}) @@ -241,7 +240,7 @@ def get_value(field_name: str, field_info: FieldInfo) -> Any: return GeneralManager.__get_building_mode_value(general) path = field_info["path"] - study_ver = file_study.config.version + study_ver = study.version start_ver = cast(int, field_info.get("start_version", 0)) end_ver = cast(int, field_info.get("end_version", study_ver)) target_name = path.split("/")[-1] @@ -252,30 +251,27 @@ def get_value(field_name: str, field_info: FieldInfo) -> Any: return GeneralFormFields.model_construct(**{name: get_value(name, info) for name, info in FIELDS_INFO.items()}) - def set_field_values(self, study: Study, field_values: GeneralFormFields) -> None: + def set_field_values(self, study: StudyInterface, field_values: GeneralFormFields) -> None: """ Set Optimization config from the webapp form """ commands: List[UpdateConfig] = [] - cmd_cx = self.storage_service.variant_study_service.command_factory.command_context - file_study = self.storage_service.get_storage(study).get_raw(study) + cmd_cx = self._command_context for field_name, value in field_values.__iter__(): if value is not None: info = FIELDS_INFO[field_name] if field_name == BUILDING_MODE: - commands.extend(GeneralManager.__get_building_mode_update_cmds(value, file_study, cmd_cx)) + commands.extend(GeneralManager.__get_building_mode_update_cmds(value, study, cmd_cx)) continue commands.append( - UpdateConfig( - target=info["path"], data=value, command_context=cmd_cx, study_version=file_study.config.version - ) + UpdateConfig(target=info["path"], data=value, command_context=cmd_cx, study_version=study.version) ) if commands: - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) @staticmethod def __get_building_mode_value(general_config: Dict[str, Any]) -> str: @@ -291,17 +287,16 @@ def __get_building_mode_value(general_config: Dict[str, Any]) -> str: @staticmethod def __get_building_mode_update_cmds( new_value: BuildingMode, - file_study: FileStudy, + study: StudyInterface, cmd_context: CommandContext, ) -> List[UpdateConfig]: - study_version = file_study.config.version if new_value == BuildingMode.DERATED: return [ UpdateConfig( target=f"{GENERAL_PATH}/derated", data=True, command_context=cmd_context, - study_version=study_version, + study_version=study.version, ) ] @@ -309,14 +304,14 @@ def __get_building_mode_update_cmds( UpdateConfig( target=( f"{GENERAL_PATH}/custom-scenario" - if study_version >= STUDY_VERSION_8 + if study.version >= STUDY_VERSION_8 else f"{GENERAL_PATH}/custom-ts-numbers" ), data=new_value == BuildingMode.CUSTOM, command_context=cmd_context, - study_version=study_version, + study_version=study.version, ), UpdateConfig( - target=f"{GENERAL_PATH}/derated", data=False, command_context=cmd_context, study_version=study_version + target=f"{GENERAL_PATH}/derated", data=False, command_context=cmd_context, study_version=study.version ), ] diff --git a/antarest/study/business/link_management.py b/antarest/study/business/link_management.py index 36ad03a795..ddda84e809 100644 --- a/antarest/study/business/link_management.py +++ b/antarest/study/business/link_management.py @@ -12,26 +12,23 @@ from typing import Any, Dict, List, Mapping, Tuple -from antares.study.version import StudyVersion - from antarest.core.exceptions import LinkNotFound from antarest.core.model import JSON from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO, LinkInternal -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import RawStudy, Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_link import CreateLink from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink from antarest.study.storage.variantstudy.model.command.update_link import UpdateLink +from antarest.study.storage.variantstudy.model.command_context import CommandContext class LinkManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_all_links(self, study: Study) -> List[LinkDTO]: - file_study = self.storage_service.get_storage(study).get_raw(study) + def get_all_links(self, study: StudyInterface) -> List[LinkDTO]: + file_study = study.get_files() result: List[LinkDTO] = [] for area_id, area in file_study.config.areas.items(): @@ -47,8 +44,8 @@ def get_all_links(self, study: Study) -> List[LinkDTO]: return result - def get_link(self, study: RawStudy, link: LinkInternal) -> LinkInternal: - file_study = self.storage_service.get_storage(study).get_raw(study) + def get_link(self, study: StudyInterface, link: LinkInternal) -> LinkInternal: + file_study = study.get_files() link_properties = self._get_link_if_exists(file_study, link) @@ -58,29 +55,26 @@ def get_link(self, study: RawStudy, link: LinkInternal) -> LinkInternal: return updated_link - def create_link(self, study: Study, link_creation_dto: LinkDTO) -> LinkDTO: - link = link_creation_dto.to_internal(StudyVersion.parse(study.version)) - - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) + def create_link(self, study: StudyInterface, link_creation_dto: LinkDTO) -> LinkDTO: + link = link_creation_dto.to_internal(study.version) command = CreateLink( area1=link.area1, area2=link.area2, parameters=link.model_dump(exclude_none=True), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) return link_creation_dto - def update_link(self, study: RawStudy, area_from: str, area_to: str, link_update_dto: LinkBaseDTO) -> LinkDTO: + def update_link(self, study: StudyInterface, area_from: str, area_to: str, link_update_dto: LinkBaseDTO) -> LinkDTO: link_dto = LinkDTO(area1=area_from, area2=area_to, **link_update_dto.model_dump(exclude_unset=True)) - link = link_dto.to_internal(StudyVersion.parse(study.version)) - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() + link = link_dto.to_internal(study.version) self._get_link_if_exists(file_study, link) @@ -90,11 +84,11 @@ def update_link(self, study: RawStudy, area_from: str, area_to: str, link_update parameters=link.model_dump( include=link_update_dto.model_fields_set, exclude={"area1", "area2"}, exclude_none=True ), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) updated_link = self.get_link(study, link) @@ -102,7 +96,7 @@ def update_link(self, study: RawStudy, area_from: str, area_to: str, link_update def update_links( self, - study: RawStudy, + study: StudyInterface, update_links_by_ids: Mapping[Tuple[str, str], LinkBaseDTO], ) -> Mapping[Tuple[str, str], LinkBaseDTO]: new_links_by_ids = {} @@ -112,15 +106,14 @@ def update_links( return new_links_by_ids - def delete_link(self, study: RawStudy, area1_id: str, area2_id: str) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) + def delete_link(self, study: StudyInterface, area1_id: str, area2_id: str) -> None: command = RemoveLink( area1=area1_id, area2=area2_id, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) def _get_link_if_exists(self, file_study: FileStudy, link: LinkInternal) -> dict[str, Any]: try: diff --git a/antarest/study/business/matrix_management.py b/antarest/study/business/matrix_management.py index bc61aaa098..9fd4ff0e5c 100644 --- a/antarest/study/business/matrix_management.py +++ b/antarest/study/business/matrix_management.py @@ -19,13 +19,11 @@ import pandas as pd from antarest.matrixstore.matrix_editor import MatrixEditInstruction, MatrixSlice, Operation -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.utils import is_managed from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix +from antarest.study.storage.variantstudy.model.command_context import CommandContext logger = logging.getLogger(__name__) @@ -231,19 +229,18 @@ def merge_edit_instructions( class MatrixManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context def update_matrix( self, - study: Study, + study: StudyInterface, path: str, edit_instructions: List[MatrixEditInstruction], ) -> None: logger.info(f"Starting matrix update for {study.id}...") - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) - matrix_service = self.storage_service.variant_study_service.command_factory.command_context.matrix_service + file_study = study.get_files() + matrix_service = self._command_context.matrix_service matrix_node = file_study.tree.get_node(url=path.split("/")) @@ -290,22 +287,12 @@ def update_matrix( ReplaceMatrix( target=path, matrix=strip_matrix_protocol(new_matrix_id), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) ] logger.info(f"Executing command for study '{study.id}'...") - execute_or_add_commands( - study=study, - file_study=file_study, - commands=command, - storage_service=self.storage_service, - ) - - if not is_managed(study): - logger.info(f"Denormalizing matrix for path '{path}'...") - matrix_node = file_study.tree.get_node(path.split("/")) - matrix_node.denormalize() + study.add_commands(command) logger.info("Matrix update done.") diff --git a/antarest/study/business/optimization_management.py b/antarest/study/business/optimization_management.py index 1912f2b372..e9b3865eb3 100644 --- a/antarest/study/business/optimization_management.py +++ b/antarest/study/business/optimization_management.py @@ -16,10 +16,10 @@ from antarest.study.business.all_optional_meta import all_optional_model from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import GENERAL_DATA_PATH, FieldInfo, FormFieldsBaseModel from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class LegacyTransmissionCapacities(EnumIgnoreCase): @@ -118,29 +118,28 @@ class OptimizationFormFields(FormFieldsBaseModel): class OptimizationManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_field_values(self, study: Study) -> OptimizationFormFields: + def get_field_values(self, study: StudyInterface) -> OptimizationFormFields: """ Get optimization field values for the webapp form """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() general_data = file_study.tree.get(GENERAL_DATA_PATH.split("/")) parent = general_data.get("optimization", {}) def get_value(field_info: FieldInfo) -> Any: path = field_info["path"] - study_ver = file_study.config.version start_ver = cast(int, field_info.get("start_version", 0)) target_name = path.split("/")[-1] - is_in_version = start_ver <= study_ver + is_in_version = start_ver <= study.version return parent.get(target_name, field_info["default_value"]) if is_in_version else None return OptimizationFormFields.model_construct(**{name: get_value(info) for name, info in FIELDS_INFO.items()}) - def set_field_values(self, study: Study, field_values: OptimizationFormFields) -> None: + def set_field_values(self, study: StudyInterface, field_values: OptimizationFormFields) -> None: """ Set optimization config from the webapp form """ @@ -154,11 +153,10 @@ def set_field_values(self, study: Study, field_values: OptimizationFormFields) - UpdateConfig( target=info["path"], data=value, - command_context=self.storage_service.variant_study_service.command_factory.command_context, + command_context=self._command_context, study_version=study.version, ) ) if commands: - file_study = self.storage_service.get_storage(study).get_raw(study) - execute_or_add_commands(study, file_study, commands, self.storage_service) + study.add_commands(commands) diff --git a/antarest/study/business/playlist_management.py b/antarest/study/business/playlist_management.py index 30a93d56c2..5a12d49bf6 100644 --- a/antarest/study/business/playlist_management.py +++ b/antarest/study/business/playlist_management.py @@ -15,11 +15,11 @@ from pydantic.types import StrictBool, StrictFloat, StrictInt from antarest.study.business.general_management import FIELDS_INFO -from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands -from antarest.study.model import RawStudy +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import FormFieldsBaseModel from antarest.study.storage.rawstudy.model.helpers import FileStudyHelpers -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.update_playlist import UpdatePlaylist +from antarest.study.storage.variantstudy.model.command_context import CommandContext DEFAULT_WEIGHT = 1 @@ -30,14 +30,14 @@ class PlaylistColumns(FormFieldsBaseModel): class PlaylistManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context def get_table_data( self, - study: RawStudy, + study: StudyInterface, ) -> Dict[int, PlaylistColumns]: - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() playlist = FileStudyHelpers.get_playlist(file_study) or {} nb_years = file_study.tree.get(FIELDS_INFO["nb_years"]["path"].split("/")) or len(playlist) @@ -52,11 +52,9 @@ def get_table_data( def set_table_data( self, - study: RawStudy, + study: StudyInterface, data: Dict[int, PlaylistColumns], ) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) - years_by_bool: Dict[bool, List[int]] = {True: [], False: []} for year, col in data.items(): years_by_bool[col.status].append(year - 1) @@ -65,17 +63,11 @@ def set_table_data( weights = {year: col.weight for year, col in data.items() if col.weight != DEFAULT_WEIGHT} - execute_or_add_commands( - study, - file_study, - [ - UpdatePlaylist( - items=active_playlists, - weights=weights, - active=True, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ) - ], - self.storage_service, + command = UpdatePlaylist( + items=active_playlists, + weights=weights, + active=True, + command_context=self._command_context, + study_version=study.version, ) + study.add_commands([command]) diff --git a/antarest/study/business/scenario_builder_management.py b/antarest/study/business/scenario_builder_management.py index 513797af06..1adfaaa58b 100644 --- a/antarest/study/business/scenario_builder_management.py +++ b/antarest/study/business/scenario_builder_management.py @@ -16,12 +16,11 @@ import typing_extensions as te from typing_extensions import override -from antarest.study.business.utils import execute_or_add_commands -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.filesystem.config.ruleset_matrices import RulesetMatrices, TableForm from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.update_scenario_builder import UpdateScenarioBuilder +from antarest.study.storage.variantstudy.model.command_context import CommandContext # Symbols used in scenario builder data _AREA_RELATED_SYMBOLS = "l", "h", "w", "s", "bc", "hgp" @@ -162,11 +161,11 @@ def _build_ruleset(file_study: FileStudy, symbol: str = "") -> RulesetMatrices: class ScenarioBuilderManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_config(self, study: Study) -> Rulesets: - sections = cast(_Sections, self.storage_service.get_storage(study).get(study, "/settings/scenariobuilder")) + def get_config(self, study: StudyInterface) -> Rulesets: + sections = cast(_Sections, study.get_files().tree.get(["settings", "scenariobuilder"])) rulesets: Rulesets = {} for ruleset_name, data in sections.items(): @@ -192,8 +191,7 @@ def get_config(self, study: Study) -> Rulesets: return rulesets - def update_config(self, study: Study, rulesets: Rulesets) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) + def update_config(self, study: StudyInterface, rulesets: Rulesets) -> None: sections: _Sections = {} for ruleset_name, ruleset in rulesets.items(): @@ -210,17 +208,14 @@ def update_config(self, study: Study, rulesets: Rulesets) -> None: else: # pragma: no cover raise NotImplementedError(f"Unknown symbol {symbol}") - context = self.storage_service.variant_study_service.command_factory.command_context - execute_or_add_commands( - study, - file_study, - [UpdateScenarioBuilder(data=sections, command_context=context, study_version=file_study.config.version)], - self.storage_service, + command = UpdateScenarioBuilder( + data=sections, command_context=self._command_context, study_version=study.version ) + study.add_commands([command]) - def get_scenario_by_type(self, study: Study, scenario_type: ScenarioType) -> TableForm: + def get_scenario_by_type(self, study: StudyInterface, scenario_type: ScenarioType) -> TableForm: symbol = SYMBOLS_BY_SCENARIO_TYPES[scenario_type] - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() ruleset = _build_ruleset(file_study, symbol) ruleset.sort_scenarios() @@ -228,8 +223,10 @@ def get_scenario_by_type(self, study: Study, scenario_type: ScenarioType) -> Tab table_form = ruleset.get_table_form(str(scenario_type), nan_value="") return table_form - def update_scenario_by_type(self, study: Study, table_form: TableForm, scenario_type: ScenarioType) -> TableForm: - file_study = self.storage_service.get_storage(study).get_raw(study) + def update_scenario_by_type( + self, study: StudyInterface, table_form: TableForm, scenario_type: ScenarioType + ) -> TableForm: + file_study = study.get_files() ruleset = _build_ruleset(file_study) ruleset.update_table_form(table_form, str(scenario_type), nan_value="") ruleset.sort_scenarios() @@ -237,11 +234,10 @@ def update_scenario_by_type(self, study: Study, table_form: TableForm, scenario_ # Create the UpdateScenarioBuilder command ruleset_name = _get_active_ruleset_name(file_study) data = {ruleset_name: ruleset.get_rules(allow_nan=True)} - command_context = self.storage_service.variant_study_service.command_factory.command_context update_scenario = UpdateScenarioBuilder( - data=data, command_context=command_context, study_version=file_study.config.version + data=data, command_context=self._command_context, study_version=study.version ) - execute_or_add_commands(study, file_study, [update_scenario], self.storage_service) + study.add_commands([update_scenario]) # Extract the updated table form for the given scenario type table_form = ruleset.get_table_form(str(scenario_type), nan_value="") diff --git a/antarest/study/business/study_interface.py b/antarest/study/business/study_interface.py new file mode 100644 index 0000000000..b441598efc --- /dev/null +++ b/antarest/study/business/study_interface.py @@ -0,0 +1,119 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Sequence + +from antares.study.version import StudyVersion +from typing_extensions import override + +from antarest.core.exceptions import CommandApplicationError +from antarest.study.model import Patch +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.variantstudy.model.command.icommand import ICommand + + +class StudyInterface(ABC): + """ + Business domain managers can read data and add commands to a study + through this interface. + """ + + @property + @abstractmethod + def id(self) -> str: + raise NotImplementedError() + + @property + @abstractmethod + def version(self) -> StudyVersion: + raise NotImplementedError() + + # TODO: in the end this should provide a read-only DAO which encapsulates + # the actual storage implementation + @abstractmethod + def get_files(self) -> FileStudy: + """ + Gets the file representation of the study. + + This is meant to be a "read-only" access to the study, + modifications should be made through commands. + """ + raise NotImplementedError() + + @abstractmethod + def add_commands( + self, + commands: Sequence[ICommand], + ) -> None: + """ + Adds commands to that study. + Note that implementations are not required to actually modify the underlying file study. + """ + raise NotImplementedError() + + # TODO: Technical debt, see if it's still necessary or remove it. + def get_patch_data(self) -> Patch: + raise NotImplementedError() + + # TODO: Technical debt, see if it's still necessary. + def update_patch_data(self, patch_data: Patch) -> None: + """ + Adds commands to that study. + Note that implementations are not required to actually modify the underlying file study. + """ + raise NotImplementedError() + + +class FileStudyInterface(StudyInterface): + """ + Basic implementation of study interface. + Only used for test purposes, currently. + """ + + def __init__(self, file_study: FileStudy): + self.file_study = file_study + + @override + @property + def id(self) -> str: + return self.file_study.config.study_id + + @override + @property + def version(self) -> StudyVersion: + return self.file_study.config.version + + @override + def get_files(self) -> FileStudy: + return self.file_study + + @override + def add_commands(self, commands: Sequence[ICommand]) -> None: + for command in commands: + result = command.apply(self.file_study) + if not result.status: + raise CommandApplicationError(result.message) + + @override + def get_patch_data(self) -> Patch: + patch = Patch() + patch_path = self.file_study.config.study_path / "patch.json" + if patch_path.exists(): + patch = Patch.model_validate_json(patch_path.read_bytes()) + return patch + + @override + def update_patch_data(self, patch_data: Patch) -> None: + patch_path = self.file_study.config.study_path / "patch.json" + patch_path.parent.mkdir(parents=True, exist_ok=True) + patch_path.write_text(patch_data.model_dump_json()) diff --git a/antarest/study/business/table_mode_management.py b/antarest/study/business/table_mode_management.py index 8faa5eebcc..eaf624e6c9 100644 --- a/antarest/study/business/table_mode_management.py +++ b/antarest/study/business/table_mode_management.py @@ -15,7 +15,6 @@ import numpy as np import pandas as pd -from antares.study.version import StudyVersion from typing_extensions import override from antarest.core.exceptions import ChildNotFoundError @@ -29,7 +28,8 @@ from antarest.study.business.link_management import LinkManager from antarest.study.business.model.area_model import AreaOutput from antarest.study.business.model.link_model import LinkBaseDTO -from antarest.study.model import STUDY_VERSION_8_2, RawStudy +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import STUDY_VERSION_8_2 _TableIndex = str # row name _TableColumn = str # column name @@ -97,17 +97,13 @@ def __init__( self._st_storage_manager = st_storage_manager self._binding_constraint_manager = binding_constraint_manager - def _get_table_data_unsafe(self, study: RawStudy, table_type: TableModeType) -> TableDataDTO: + def _get_table_data_unsafe(self, study: StudyInterface, table_type: TableModeType) -> TableDataDTO: if table_type == TableModeType.AREA: areas_map = self._area_manager.get_all_area_props(study) data = {area_id: area.model_dump(mode="json", by_alias=True) for area_id, area in areas_map.items()} elif table_type == TableModeType.LINK: links_map = self._link_manager.get_all_links(study) - excludes = ( - set() - if StudyVersion.parse(study.version) >= STUDY_VERSION_8_2 - else {"filter_synthesis", "filter_year_by_year"} - ) + excludes = set() if study.version >= STUDY_VERSION_8_2 else {"filter_synthesis", "filter_year_by_year"} data = { f"{link.area1} / {link.area2}": link.model_dump(mode="json", by_alias=True, exclude=excludes) for link in links_map @@ -142,7 +138,7 @@ def _get_table_data_unsafe(self, study: RawStudy, table_type: TableModeType) -> def get_table_data( self, - study: RawStudy, + study: StudyInterface, table_type: TableModeType, columns: Sequence[_TableColumn], ) -> TableDataDTO: @@ -180,7 +176,7 @@ def get_table_data( def update_table_data( self, - study: RawStudy, + study: StudyInterface, table_type: TableModeType, data: TableDataDTO, ) -> TableDataDTO: @@ -205,11 +201,7 @@ def update_table_data( elif table_type == TableModeType.LINK: links_map = {tuple(key.split(" / ")): LinkBaseDTO(**values) for key, values in data.items()} updated_map = self._link_manager.update_links(study, links_map) # type: ignore - excludes = ( - set() - if StudyVersion.parse(study.version) >= STUDY_VERSION_8_2 - else {"filter_synthesis", "filter_year_by_year"} - ) + excludes = set() if study.version >= STUDY_VERSION_8_2 else {"filter_synthesis", "filter_year_by_year"} data = { f"{area1_id} / {area2_id}": link.model_dump(by_alias=True, exclude=excludes) for (area1_id, area2_id), link in updated_map.items() diff --git a/antarest/study/business/thematic_trimming_management.py b/antarest/study/business/thematic_trimming_management.py index 83d4afc674..17bf022f16 100644 --- a/antarest/study/business/thematic_trimming_management.py +++ b/antarest/study/business/thematic_trimming_management.py @@ -12,24 +12,22 @@ from typing import Any, Dict, List, Mapping, cast -from antares.study.version import StudyVersion - +from antarest.study.business.study_interface import StudyInterface from antarest.study.business.thematic_trimming_field_infos import ThematicTrimmingFormFields, get_fields_info -from antarest.study.business.utils import GENERAL_DATA_PATH, execute_or_add_commands -from antarest.study.model import Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.utils import GENERAL_DATA_PATH from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class ThematicTrimmingManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_field_values(self, study: Study) -> ThematicTrimmingFormFields: + def get_field_values(self, study: StudyInterface) -> ThematicTrimmingFormFields: """ Get Thematic Trimming field values for the webapp form """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() config = file_study.tree.get(GENERAL_DATA_PATH.split("/")) trimming_config = config.get("variables selection") or {} exclude_vars = trimming_config.get("select_var -") or [] @@ -42,19 +40,18 @@ def get_value(field_info: Mapping[str, Any]) -> bool: var_name = field_info["path"] return var_name not in exclude_vars if selected_vars_reset else var_name in include_vars - fields_info = get_fields_info(StudyVersion.parse(study.version)) + fields_info = get_fields_info(study.version) fields_values = {name: get_value(info) for name, info in fields_info.items()} return ThematicTrimmingFormFields(**fields_values) - def set_field_values(self, study: Study, field_values: ThematicTrimmingFormFields) -> None: + def set_field_values(self, study: StudyInterface, field_values: ThematicTrimmingFormFields) -> None: """ Set Thematic Trimming config from the webapp form """ - file_study = self.storage_service.get_storage(study).get_raw(study) field_values_dict = field_values.model_dump(mode="json") keys_by_bool: Dict[bool, List[Any]] = {True: [], False: []} - fields_info = get_fields_info(StudyVersion.parse(study.version)) + fields_info = get_fields_info(study.version) for name, info in fields_info.items(): keys_by_bool[field_values_dict[name]].append(info["path"]) @@ -70,16 +67,10 @@ def set_field_values(self, study: Study, field_values: ThematicTrimmingFormField "select_var +": keys_by_bool[True], } - execute_or_add_commands( - study, - file_study, - [ - UpdateConfig( - target="settings/generaldata/variables selection", - data=config_data, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ) - ], - self.storage_service, + command = UpdateConfig( + target="settings/generaldata/variables selection", + data=config_data, + command_context=self._command_context, + study_version=study.version, ) + study.add_commands([command]) diff --git a/antarest/study/business/timeseries_config_management.py b/antarest/study/business/timeseries_config_management.py index 853fd832f1..657dbac0b9 100644 --- a/antarest/study/business/timeseries_config_management.py +++ b/antarest/study/business/timeseries_config_management.py @@ -12,10 +12,10 @@ from antarest.core.serde import AntaresBaseModel from antarest.study.business.all_optional_meta import all_optional_model -from antarest.study.business.utils import GENERAL_DATA_PATH, execute_or_add_commands -from antarest.study.model import Study -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.business.study_interface import StudyInterface +from antarest.study.business.utils import GENERAL_DATA_PATH from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig +from antarest.study.storage.variantstudy.model.command_context import CommandContext class TimeSeriesTypeConfig(AntaresBaseModel, extra="forbid", validate_assignment=True, populate_by_name=True): @@ -28,14 +28,14 @@ class TimeSeriesConfigDTO(AntaresBaseModel, extra="forbid", validate_assignment= class TimeSeriesConfigManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, command_context: CommandContext) -> None: + self._command_context = command_context - def get_values(self, study: Study) -> TimeSeriesConfigDTO: + def get_values(self, study: StudyInterface) -> TimeSeriesConfigDTO: """ Get Time-Series generation values """ - file_study = self.storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() url = GENERAL_DATA_PATH.split("/") url.extend(["general", "nbtimeseriesthermal"]) nb_ts_gen_thermal = file_study.tree.get(url) @@ -43,18 +43,17 @@ def get_values(self, study: Study) -> TimeSeriesConfigDTO: args = {"thermal": TimeSeriesTypeConfig(number=nb_ts_gen_thermal)} return TimeSeriesConfigDTO.model_validate(args) - def set_values(self, study: Study, field_values: TimeSeriesConfigDTO) -> None: + def set_values(self, study: StudyInterface, field_values: TimeSeriesConfigDTO) -> None: """ Set Time-Series generation values """ - file_study = self.storage_service.get_storage(study).get_raw(study) if field_values.thermal: url = f"{GENERAL_DATA_PATH}/general/nbtimeseriesthermal" command = UpdateConfig( target=url, data=field_values.thermal.number, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, + command_context=self._command_context, + study_version=study.version, ) - execute_or_add_commands(study, file_study, [command], self.storage_service) + study.add_commands([command]) diff --git a/antarest/study/business/xpansion_management.py b/antarest/study/business/xpansion_management.py index 70e9194676..0f6dd347ad 100644 --- a/antarest/study/business/xpansion_management.py +++ b/antarest/study/business/xpansion_management.py @@ -26,10 +26,10 @@ from antarest.core.serde import AntaresBaseModel from antarest.study.business.all_optional_meta import all_optional_model from antarest.study.business.enum_ignore_case import EnumIgnoreCase -from antarest.study.model import Study +from antarest.study.business.study_interface import StudyInterface from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.utils import fix_study_root +from antarest.study.storage.variantstudy.model.command_context import CommandContext logger = logging.getLogger(__name__) @@ -301,12 +301,12 @@ def __init__(self, message: str) -> None: class XpansionManager: - def __init__(self, study_storage_service: StudyStorageService): - self.study_storage_service = study_storage_service + def __init__(self, command_context: CommandContext): + self._command_context = command_context - def create_xpansion_configuration(self, study: Study, zipped_config: Optional[UploadFile] = None) -> None: + def create_xpansion_configuration(self, study: StudyInterface, zipped_config: Optional[UploadFile] = None) -> None: logger.info(f"Initiating xpansion configuration for study '{study.id}'") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() try: file_study.tree.get(["user", "expansion"]) logger.info(f"Using existing configuration for study '{study.id}'") @@ -354,14 +354,14 @@ def create_xpansion_configuration(self, study: Study, zipped_config: Optional[Up file_study.tree.save(xpansion_configuration_data) - def delete_xpansion_configuration(self, study: Study) -> None: + def delete_xpansion_configuration(self, study: StudyInterface) -> None: logger.info(f"Deleting xpansion configuration for study '{study.id}'") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() file_study.tree.delete(["user", "expansion"]) - def get_xpansion_settings(self, study: Study) -> GetXpansionSettings: + def get_xpansion_settings(self, study: StudyInterface) -> GetXpansionSettings: logger.info(f"Getting xpansion settings for study '{study.id}'") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() config_obj = file_study.tree.get(["user", "expansion", "settings"]) with contextlib.suppress(ChildNotFoundError): config_obj["sensitivity_config"] = file_study.tree.get( @@ -370,7 +370,7 @@ def get_xpansion_settings(self, study: Study) -> GetXpansionSettings: return GetXpansionSettings.from_config(config_obj) def update_xpansion_settings( - self, study: Study, new_xpansion_settings: UpdateXpansionSettings + self, study: StudyInterface, new_xpansion_settings: UpdateXpansionSettings ) -> GetXpansionSettings: logger.info(f"Updating xpansion settings for study '{study.id}'") @@ -380,7 +380,7 @@ def update_xpansion_settings( ) updated_settings = actual_settings.model_copy(deep=True, update=settings_fields) - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() # Specific handling for yearly_weights and additional_constraints: # - If the attributes are given, it means that the user wants to select a file. @@ -538,8 +538,8 @@ def _assert_candidate_is_correct( ) assert xpansion_candidate_dto.annual_cost_per_mw - def add_candidate(self, study: Study, xpansion_candidate: XpansionCandidateDTO) -> XpansionCandidateDTO: - file_study = self.study_storage_service.get_storage(study).get_raw(study) + def add_candidate(self, study: StudyInterface, xpansion_candidate: XpansionCandidateDTO) -> XpansionCandidateDTO: + file_study = study.get_files() candidates_obj = file_study.tree.get(["user", "expansion", "candidates"]) @@ -558,10 +558,10 @@ def add_candidate(self, study: Study, xpansion_candidate: XpansionCandidateDTO) # Should we add a field in the study config containing the xpansion candidates like the links or the areas ? return self.get_candidate(study, xpansion_candidate.name) - def get_candidate(self, study: Study, candidate_name: str) -> XpansionCandidateDTO: + def get_candidate(self, study: StudyInterface, candidate_name: str) -> XpansionCandidateDTO: logger.info(f"Getting candidate '{candidate_name}' of study '{study.id}'") # This takes the first candidate with the given name and not the id, because the name is the primary key. - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() candidates = file_study.tree.get(["user", "expansion", "candidates"]) try: candidate = next(c for c in candidates.values() if c["name"] == candidate_name) @@ -570,19 +570,19 @@ def get_candidate(self, study: Study, candidate_name: str) -> XpansionCandidateD except StopIteration: raise CandidateNotFoundError(f"The candidate '{candidate_name}' does not exist") - def get_candidates(self, study: Study) -> List[XpansionCandidateDTO]: + def get_candidates(self, study: StudyInterface) -> List[XpansionCandidateDTO]: logger.info(f"Getting all candidates of study {study.id}") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() candidates = file_study.tree.get(["user", "expansion", "candidates"]) return [XpansionCandidateDTO(**c) for c in candidates.values()] def update_candidate( self, - study: Study, + study: StudyInterface, candidate_name: str, xpansion_candidate_dto: XpansionCandidateDTO, ) -> None: - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() candidates = file_study.tree.get(["user", "expansion", "candidates"]) @@ -600,8 +600,8 @@ def update_candidate( return raise CandidateNotFoundError(f"The candidate '{xpansion_candidate_dto.name}' does not exist") - def delete_candidate(self, study: Study, candidate_name: str) -> None: - file_study = self.study_storage_service.get_storage(study).get_raw(study) + def delete_candidate(self, study: StudyInterface, candidate_name: str) -> None: + file_study = study.get_files() candidates = file_study.tree.get(["user", "expansion", "candidates"]) candidate_id = next( @@ -611,7 +611,9 @@ def delete_candidate(self, study: Study, candidate_name: str) -> None: logger.info(f"Deleting candidate '{candidate_name}' from study '{study.id}'") file_study.tree.delete(["user", "expansion", "candidates", candidate_id]) - def update_xpansion_constraints_settings(self, study: Study, constraints_file_name: str) -> GetXpansionSettings: + def update_xpansion_constraints_settings( + self, study: StudyInterface, constraints_file_name: str + ) -> GetXpansionSettings: # Make sure filename is not `None`, because `None` values are ignored by the update. constraints_file_name = constraints_file_name or "" # noinspection PyArgumentList @@ -664,21 +666,21 @@ def _add_raw_files( def add_resource( self, - study: Study, + study: StudyInterface, resource_type: XpansionResourceFileType, files: List[UploadFile], ) -> None: logger.info(f"Adding xpansion {resource_type} resource file list to study '{study.id}'") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() self._add_raw_files(file_study, files, resource_type) def delete_resource( self, - study: Study, + study: StudyInterface, resource_type: XpansionResourceFileType, filename: str, ) -> None: - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() logger.info( f"Checking if xpansion {resource_type} resource file '{filename}' is not used in study '{study.id}'" ) @@ -700,17 +702,17 @@ def delete_resource( def get_resource_content( self, - study: Study, + study: StudyInterface, resource_type: XpansionResourceFileType, filename: str, ) -> JSON | bytes: logger.info(f"Getting xpansion {resource_type} resource file '{filename}' from study '{study.id}'") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() return file_study.tree.get(self._raw_file_dir(resource_type) + [filename]) - def list_resources(self, study: Study, resource_type: XpansionResourceFileType) -> List[str]: + def list_resources(self, study: StudyInterface, resource_type: XpansionResourceFileType) -> List[str]: logger.info(f"Getting all xpansion {resource_type} files from study '{study.id}'") - file_study = self.study_storage_service.get_storage(study).get_raw(study) + file_study = study.get_files() try: return [filename for filename in file_study.tree.get(self._raw_file_dir(resource_type)).keys()] except ChildNotFoundError: diff --git a/antarest/study/main.py b/antarest/study/main.py index f992157597..b0635f4910 100644 --- a/antarest/study/main.py +++ b/antarest/study/main.py @@ -97,7 +97,6 @@ def build_study_service( command_factory = CommandFactory( generator_matrix_constants=generator_matrix_constants, matrix_service=matrix_service, - patch_service=patch_service, ) variant_study_service = VariantStudyService( task_service=task_service, @@ -114,6 +113,7 @@ def build_study_service( study_service = study_service or StudyService( raw_study_service=raw_study_service, variant_study_service=variant_study_service, + command_context=command_factory.command_context, user_service=user_service, repository=metadata_repository, event_bus=event_bus, diff --git a/antarest/study/model.py b/antarest/study/model.py index df0361c2f4..06bc2bb94d 100644 --- a/antarest/study/model.py +++ b/antarest/study/model.py @@ -314,13 +314,13 @@ class RawStudy(Study): __tablename__ = "rawstudy" - id = Column( + id: str = Column( String(36), ForeignKey("study.id", ondelete="CASCADE"), primary_key=True, ) - content_status = Column(Enum(StudyContentStatus)) - workspace = Column(String(255), default=DEFAULT_WORKSPACE_NAME, nullable=False, index=True) + content_status: StudyContentStatus = Column(Enum(StudyContentStatus)) + workspace: str = Column(String(255), default=DEFAULT_WORKSPACE_NAME, nullable=False, index=True) missing = Column(DateTime, nullable=True, index=True) __mapper_args__ = { diff --git a/antarest/study/service.py b/antarest/study/service.py index b402c9330b..4fee65237c 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -20,7 +20,7 @@ import time from datetime import datetime, timedelta from pathlib import Path, PurePosixPath -from typing import Any, BinaryIO, Callable, Dict, List, Optional, Sequence, Tuple, Type, cast +from typing import Any, BinaryIO, Callable, Dict, List, MutableSequence, Optional, Sequence, Tuple, Type, cast from uuid import uuid4 import numpy as np @@ -68,6 +68,7 @@ from antarest.core.utils.utils import StopWatch from antarest.login.model import Group from antarest.login.service import LoginService +from antarest.login.utils import get_current_user from antarest.matrixstore.matrix_editor import MatrixEditInstruction from antarest.study.business.adequacy_patch_management import AdequacyPatchManager from antarest.study.business.advanced_parameters_management import AdvancedParamsManager @@ -97,6 +98,7 @@ from antarest.study.business.optimization_management import OptimizationManager from antarest.study.business.playlist_management import PlaylistManager from antarest.study.business.scenario_builder_management import ScenarioBuilderManager +from antarest.study.business.study_interface import StudyInterface from antarest.study.business.table_mode_management import TableModeManager from antarest.study.business.thematic_trimming_management import ThematicTrimmingManager from antarest.study.business.timeseries_config_management import TimeSeriesConfigManager @@ -114,6 +116,7 @@ CommentsDto, ExportFormat, MatrixIndex, + Patch, PatchArea, PatchCluster, RawStudy, @@ -135,7 +138,9 @@ StudySortBy, ) from antarest.study.storage.matrix_profile import adjust_matrix_columns_index +from antarest.study.storage.patch_service import PatchService from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfigDTO +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode from antarest.study.storage.rawstudy.model.filesystem.inode import INode, OriginalFile from antarest.study.storage.rawstudy.model.filesystem.matrix.input_series_matrix import InputSeriesMatrix @@ -175,6 +180,7 @@ from antarest.study.storage.variantstudy.model.command.update_comments import UpdateComments from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command.update_raw_file import UpdateRawFile +from antarest.study.storage.variantstudy.model.command_context import CommandContext from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy from antarest.study.storage.variantstudy.model.model import CommandDTO @@ -385,6 +391,130 @@ def run_task(self, notifier: ITaskNotifier) -> TaskResult: __call__ = run_task +class RawStudyInterface(StudyInterface): + """ + Raw study business domain interface. + + Provides data from raw study service and applies commands instantly + on underlying files. + """ + + def __init__( + self, + raw_service: RawStudyService, + variant_service: VariantStudyService, + patch_service: PatchService, + study: RawStudy, + ): + self._raw_study_service = raw_service + self._variant_study_service = variant_service + self._patch_service = patch_service + self._study = study + self._cached_file_study: Optional[FileStudy] = None + self._version = StudyVersion.parse(self._study.version) + + @override + @property + def id(self) -> str: + return self._study.id + + @override + @property + def version(self) -> StudyVersion: + return self._version + + @override + def get_files(self) -> FileStudy: + if not self._cached_file_study: + self._cached_file_study = self._raw_study_service.get_raw(self._study) + return self._cached_file_study + + @override + def add_commands(self, commands: Sequence[ICommand]) -> None: + study = self._study + file_study = self.get_files() + + for command in commands: + result = command.apply(file_study) + if not result.status: + raise CommandApplicationError(result.message) + self._variant_study_service.invalidate_cache(study) + + if not is_managed(study): + # In a previous version, de-normalization was performed asynchronously. + # However, this cause problems with concurrent file access, + # especially when de-normalizing a matrix (which can take time). + # + # async_denormalize = threading.Thread( + # name=f"async_denormalize-{study.id}", + # target=file_study.tree.denormalize, + # ) + # async_denormalize.start() + # + # To avoid this concurrency problem, it would be necessary to implement a + # locking system for the entire study using a file lock (since multiple processes, + # not only multiple threads, could access the same content simultaneously). + # + # Currently, we use a synchronous call to address the concurrency problem + # within the current process (not across multiple processes)... + file_study.tree.denormalize() + + @override + def get_patch_data(self) -> Patch: + return self._patch_service.get(self._study) + + @override + def update_patch_data(self, patch_data: Patch) -> None: + self._patch_service.save(self._study, patch_data) + + +class VariantStudyInterface(StudyInterface): + """ + Variant study business domain interface. + + Provides data from variant study service and simply append commands + to the variant. + """ + + def __init__(self, variant_service: VariantStudyService, patch_service: PatchService, study: VariantStudy): + self._variant_service = variant_service + self._patch_service = patch_service + self._study = study + self._version = StudyVersion.parse(self._study.version) + + @override + @property + def id(self) -> str: + return self._study.id + + @override + @property + def version(self) -> StudyVersion: + return self._version + + @override + def get_files(self) -> FileStudy: + return self._variant_service.get_raw(self._study) + + @override + def add_commands(self, commands: Sequence[ICommand]) -> None: + # get current user if not in session, otherwise get session user + current_user = get_current_user() + self._variant_service.append_commands( + self._study.id, + transform_command_to_dto(commands, force_aggregate=True), + RequestParameters(user=current_user), + ) + + @override + def get_patch_data(self) -> Patch: + return self._patch_service.get(self._study) + + @override + def update_patch_data(self, patch_data: Patch) -> None: + self._patch_service.save(self._study, patch_data) + + class StudyService: """ Storage module facade service to handle studies management. @@ -394,6 +524,7 @@ def __init__( self, raw_study_service: RawStudyService, variant_study_service: VariantStudyService, + command_context: CommandContext, user_service: LoginService, repository: StudyMetadataRepository, event_bus: IEventBus, @@ -408,28 +539,28 @@ def __init__( self.event_bus = event_bus self.file_transfer_manager = file_transfer_manager self.task_service = task_service - self.area_manager = AreaManager(self.storage_service, self.repository) - self.district_manager = DistrictManager(self.storage_service) - self.links_manager = LinkManager(self.storage_service) - self.config_manager = ConfigManager(self.storage_service) - self.general_manager = GeneralManager(self.storage_service) - self.thematic_trimming_manager = ThematicTrimmingManager(self.storage_service) - self.optimization_manager = OptimizationManager(self.storage_service) - self.adequacy_patch_manager = AdequacyPatchManager(self.storage_service) - self.advanced_parameters_manager = AdvancedParamsManager(self.storage_service) - self.hydro_manager = HydroManager(self.storage_service) - self.allocation_manager = AllocationManager(self.storage_service) - self.properties_manager = PropertiesManager(self.storage_service) - self.renewable_manager = RenewableManager(self.storage_service) - self.thermal_manager = ThermalManager(self.storage_service) - self.st_storage_manager = STStorageManager(self.storage_service) - self.ts_config_manager = TimeSeriesConfigManager(self.storage_service) - self.playlist_manager = PlaylistManager(self.storage_service) - self.scenario_builder_manager = ScenarioBuilderManager(self.storage_service) - self.xpansion_manager = XpansionManager(self.storage_service) - self.matrix_manager = MatrixManager(self.storage_service) - self.binding_constraint_manager = BindingConstraintManager(self.storage_service) - self.correlation_manager = CorrelationManager(self.storage_service) + self.area_manager = AreaManager(command_context) + self.district_manager = DistrictManager(command_context) + self.links_manager = LinkManager(command_context) + self.config_manager = ConfigManager(command_context) + self.general_manager = GeneralManager(command_context) + self.thematic_trimming_manager = ThematicTrimmingManager(command_context) + self.optimization_manager = OptimizationManager(command_context) + self.adequacy_patch_manager = AdequacyPatchManager(command_context) + self.advanced_parameters_manager = AdvancedParamsManager(command_context) + self.hydro_manager = HydroManager(command_context) + self.allocation_manager = AllocationManager(command_context) + self.properties_manager = PropertiesManager(command_context) + self.renewable_manager = RenewableManager(command_context) + self.thermal_manager = ThermalManager(command_context) + self.st_storage_manager = STStorageManager(command_context) + self.ts_config_manager = TimeSeriesConfigManager(command_context) + self.playlist_manager = PlaylistManager(command_context) + self.scenario_builder_manager = ScenarioBuilderManager(command_context) + self.xpansion_manager = XpansionManager(command_context) + self.matrix_manager = MatrixManager(command_context) + self.binding_constraint_manager = BindingConstraintManager(command_context) + self.correlation_manager = CorrelationManager(command_context) self.table_mode_manager = TableModeManager( self.area_manager, self.links_manager, @@ -807,6 +938,26 @@ def check_study_access( self._assert_study_unarchived(study) return study + def get_study_interface(self, study: Study) -> StudyInterface: + """ + Creates the business interface to a particular study. + """ + if isinstance(study, VariantStudy): + return VariantStudyInterface( + self.storage_service.variant_study_service, + self.storage_service.variant_study_service.patch_service, + study, + ) + elif isinstance(study, RawStudy): + return RawStudyInterface( + self.storage_service.raw_study_service, + self.storage_service.variant_study_service, + self.storage_service.raw_study_service.patch_service, + study, + ) + else: + raise ValueError(f"Unsupported study type '{study.type}'") + def get_study_path(self, uuid: str, params: RequestParameters) -> Path: """ Retrieve study path @@ -1902,8 +2053,11 @@ def get_all_areas( ) -> List[AreaInfoDTO] | Dict[str, Any]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) + study_interface = self.get_study_interface(study) return ( - self.area_manager.get_all_areas_ui_info(study) if ui else self.area_manager.get_all_areas(study, area_type) + self.area_manager.get_all_areas_ui_info(study_interface) + if ui + else self.area_manager.get_all_areas(study_interface, area_type) ) def get_all_links( @@ -1913,7 +2067,7 @@ def get_all_links( ) -> List[LinkDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - return self.links_manager.get_all_links(study) + return self.links_manager.get_all_links(self.get_study_interface(study)) def create_area( self, @@ -1924,7 +2078,7 @@ def create_area( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - new_area = self.area_manager.create_area(study, area_creation_dto) + new_area = self.area_manager.create_area(self.get_study_interface(study), area_creation_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -1943,7 +2097,7 @@ def create_link( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - new_link = self.links_manager.create_link(study, link_creation_dto) + new_link = self.links_manager.create_link(self.get_study_interface(study), link_creation_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -1964,7 +2118,9 @@ def update_link( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - updated_link = self.links_manager.update_link(study, area_from, area_to, link_update_dto) + updated_link = self.links_manager.update_link( + self.get_study_interface(study), area_from, area_to, link_update_dto + ) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -1984,7 +2140,7 @@ def update_area( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - updated_area = self.area_manager.update_area_metadata(study, area_id, area_patch_dto) + updated_area = self.area_manager.update_area_metadata(self.get_study_interface(study), area_id, area_patch_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -2005,7 +2161,7 @@ def update_area_ui( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.area_manager.update_area_ui(study, area_id, area_ui, layer) + return self.area_manager.update_area_ui(self.get_study_interface(study), area_id, area_ui, layer) def update_thermal_cluster_metadata( self, @@ -2017,7 +2173,9 @@ def update_thermal_cluster_metadata( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.area_manager.update_thermal_cluster_metadata(study, area_id, clusters_metadata) + return self.area_manager.update_thermal_cluster_metadata( + self.get_study_interface(study), area_id, clusters_metadata + ) def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> None: """ @@ -2034,14 +2192,15 @@ def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> Non """ study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) + study_interface = self.get_study_interface(study) self._assert_study_unarchived(study) referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints( - study, ConstraintFilters(area_name=area_id) + study_interface, ConstraintFilters(area_name=area_id) ) if referencing_binding_constraints: binding_ids = [bc.id for bc in referencing_binding_constraints] raise ReferencedObjectDeletionNotAllowed(area_id, binding_ids, object_type="Area") - self.area_manager.delete_area(study, area_id) + self.area_manager.delete_area(study_interface, area_id) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -2073,14 +2232,15 @@ def delete_link( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) + study_interface = self.get_study_interface(study) link_id = LinkTerm(area1=area_from, area2=area_to).generate_id() referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints( - study, ConstraintFilters(link_id=link_id) + study_interface, ConstraintFilters(link_id=link_id) ) if referencing_binding_constraints: binding_ids = [bc.id for bc in referencing_binding_constraints] raise ReferencedObjectDeletionNotAllowed(link_id, binding_ids, object_type="Link") - self.links_manager.delete_link(study, area_from, area_to) + self.links_manager.delete_link(study_interface, area_from, area_to) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -2285,18 +2445,21 @@ def create_xpansion_configuration( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - self.xpansion_manager.create_xpansion_configuration(study, zipped_config) + study_interface = self.get_study_interface(study) + self.xpansion_manager.create_xpansion_configuration(study_interface, zipped_config) def delete_xpansion_configuration(self, uuid: str, params: RequestParameters) -> None: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - self.xpansion_manager.delete_xpansion_configuration(study) + study_interface = self.get_study_interface(study) + self.xpansion_manager.delete_xpansion_configuration(study_interface) def get_xpansion_settings(self, uuid: str, params: RequestParameters) -> GetXpansionSettings: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - return self.xpansion_manager.get_xpansion_settings(study) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.get_xpansion_settings(study_interface) def update_xpansion_settings( self, @@ -2307,7 +2470,8 @@ def update_xpansion_settings( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) self._assert_study_unarchived(study) - return self.xpansion_manager.update_xpansion_settings(study, xpansion_settings_dto) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.update_xpansion_settings(study_interface, xpansion_settings_dto) def add_candidate( self, @@ -2318,17 +2482,20 @@ def add_candidate( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.xpansion_manager.add_candidate(study, xpansion_candidate_dto) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.add_candidate(study_interface, xpansion_candidate_dto) def get_candidate(self, uuid: str, candidate_name: str, params: RequestParameters) -> XpansionCandidateDTO: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - return self.xpansion_manager.get_candidate(study, candidate_name) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.get_candidate(study_interface, candidate_name) def get_candidates(self, uuid: str, params: RequestParameters) -> List[XpansionCandidateDTO]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - return self.xpansion_manager.get_candidates(study) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.get_candidates(study_interface) def update_xpansion_candidate( self, @@ -2340,13 +2507,15 @@ def update_xpansion_candidate( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) self._assert_study_unarchived(study) - return self.xpansion_manager.update_candidate(study, candidate_name, xpansion_candidate_dto) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.update_candidate(study_interface, candidate_name, xpansion_candidate_dto) def delete_xpansion_candidate(self, uuid: str, candidate_name: str, params: RequestParameters) -> None: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) self._assert_study_unarchived(study) - return self.xpansion_manager.delete_candidate(study, candidate_name) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.delete_candidate(study_interface, candidate_name) def update_xpansion_constraints_settings( self, @@ -2357,7 +2526,8 @@ def update_xpansion_constraints_settings( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.xpansion_manager.update_xpansion_constraints_settings(study, constraints_file_name) + study_interface = self.get_study_interface(study) + return self.xpansion_manager.update_xpansion_constraints_settings(study_interface, constraints_file_name) def update_matrix( self, @@ -2384,8 +2554,9 @@ def update_matrix( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) + study_interface = self.get_study_interface(study) try: - self.matrix_manager.update_matrix(study, path, matrix_edit_instruction) + self.matrix_manager.update_matrix(study_interface, path, matrix_edit_instruction) except MatrixManagerError as exc: raise BadEditInstructionException(str(exc)) from exc @@ -2715,6 +2886,7 @@ def get_matrix_with_index_and_header( matrix_path = Path(path) study = self.get_study(study_id) + study_interface = self.get_study_interface(study) if matrix_path.parts in [("input", "hydro", "allocation"), ("input", "hydro", "correlation")]: all_areas = cast( @@ -2722,9 +2894,9 @@ def get_matrix_with_index_and_header( self.get_all_areas(study_id, area_type=AreaType.AREA, ui=False, params=parameters), ) if matrix_path.parts[-1] == "allocation": - hydro_matrix = self.allocation_manager.get_allocation_matrix(study, all_areas) + hydro_matrix = self.allocation_manager.get_allocation_matrix(study_interface, all_areas) else: - hydro_matrix = self.correlation_manager.get_correlation_matrix(all_areas, study, []) # type: ignore + hydro_matrix = self.correlation_manager.get_correlation_matrix(all_areas, study_interface, []) # type: ignore return pd.DataFrame(data=hydro_matrix.data, columns=hydro_matrix.columns, index=hydro_matrix.index) # Gets the data and checks given path existence @@ -2771,9 +2943,10 @@ def asserts_no_thermal_in_binding_constraints(self, study: Study, area_id: str, ReferencedObjectDeletionNotAllowed: if a cluster is referenced in a binding constraint """ + study_interface = self.get_study_interface(study) for cluster_id in cluster_ids: ref_bcs = self.binding_constraint_manager.get_binding_constraints( - study, ConstraintFilters(cluster_id=f"{area_id}.{cluster_id}") + study_interface, ConstraintFilters(cluster_id=f"{area_id}.{cluster_id}") ) if ref_bcs: binding_ids = [bc.id for bc in ref_bcs] @@ -2830,7 +3003,7 @@ def _alter_user_folder( args = { "data": command_data, - "study_version": StudyVersion.parse(study.version), + "study_version": study.version, "command_context": self.storage_service.variant_study_service.command_factory.command_context, } command = command_class.model_validate(args) diff --git a/antarest/study/storage/variantstudy/command_factory.py b/antarest/study/storage/variantstudy/command_factory.py index 8f9e861885..837f3523a0 100644 --- a/antarest/study/storage/variantstudy/command_factory.py +++ b/antarest/study/storage/variantstudy/command_factory.py @@ -103,12 +103,10 @@ def __init__( self, generator_matrix_constants: GeneratorMatrixConstants, matrix_service: ISimpleMatrixService, - patch_service: PatchService, ): self.command_context = CommandContext( generator_matrix_constants=generator_matrix_constants, matrix_service=matrix_service, - patch_service=patch_service, ) def _to_single_command( diff --git a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py index 04a659791d..60fa8dbd3f 100644 --- a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py @@ -36,7 +36,7 @@ from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants -from antarest.study.storage.variantstudy.business.utils import validate_matrix +from antarest.study.storage.variantstudy.business.utils import strip_matrix_protocol, validate_matrix from antarest.study.storage.variantstudy.business.utils_binding_constraint import ( parse_bindings_coeffs_and_save_into_config, ) @@ -293,7 +293,7 @@ def get_corresponding_matrices( return methods["before_v87"][time_step]() if version < 870 else methods["after_v87"][time_step]() if isinstance(v, str): # Check the matrix link - return validate_matrix(v, {"command_context": self.command_context}) + return validate_matrix(strip_matrix_protocol(v), {"command_context": self.command_context}) if isinstance(v, list): check_matrix_values(time_step, v, version) return validate_matrix(v, {"command_context": self.command_context}) diff --git a/antarest/study/storage/variantstudy/model/command_context.py b/antarest/study/storage/variantstudy/model/command_context.py index ff59a3339b..34acd7b6d2 100644 --- a/antarest/study/storage/variantstudy/model/command_context.py +++ b/antarest/study/storage/variantstudy/model/command_context.py @@ -19,7 +19,6 @@ class CommandContext(AntaresBaseModel): generator_matrix_constants: GeneratorMatrixConstants matrix_service: ISimpleMatrixService - patch_service: PatchService class Config: arbitrary_types_allowed = True diff --git a/antarest/study/storage/variantstudy/variant_study_service.py b/antarest/study/storage/variantstudy/variant_study_service.py index 0a6ee96d62..75b9fc3cc8 100644 --- a/antarest/study/storage/variantstudy/variant_study_service.py +++ b/antarest/study/storage/variantstudy/variant_study_service.py @@ -164,9 +164,8 @@ def convert_commands( self, study_id: str, api_commands: List[CommandDTOAPI], params: RequestParameters ) -> List[CommandDTO]: study = self._get_variant_study(study_id, params, raw_study_accepted=True) - study_version = StudyVersion.parse(study.version) return [ - CommandDTO.model_validate({"study_version": study_version, **command.model_dump(mode="json")}) + CommandDTO.model_validate({"study_version": study.version, **command.model_dump(mode="json")}) for command in api_commands ] diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index da8a500366..671b258bdb 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -323,7 +323,7 @@ def get_layers( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.area_manager.get_layers(study) + return study_service.area_manager.get_layers(study_service.get_study_interface(study)) @bp.post( "/studies/{uuid}/layers", @@ -342,7 +342,7 @@ def create_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.area_manager.create_layer(study, name) + return study_service.area_manager.create_layer(study_service.get_study_interface(study), name) @bp.put( "/studies/{uuid}/layers/{layer_id}", @@ -362,10 +362,11 @@ def update_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) + study_interface = study_service.get_study_interface(study) if name: - study_service.area_manager.update_layer_name(study, layer_id, name) + study_service.area_manager.update_layer_name(study_interface, layer_id, name) if areas: - study_service.area_manager.update_layer_areas(study, layer_id, areas) + study_service.area_manager.update_layer_areas(study_interface, layer_id, areas) @bp.delete( "/studies/{uuid}/layers/{layer_id}", @@ -385,7 +386,7 @@ def remove_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - study_service.area_manager.remove_layer(study, layer_id) + study_service.area_manager.remove_layer(study_service.get_study_interface(study), layer_id) @bp.get( "/studies/{uuid}/districts", @@ -403,7 +404,8 @@ def get_districts( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.district_manager.get_districts(study) + study_interface = study_service.get_study_interface(study) + return study_service.district_manager.get_districts(study_interface) @bp.post( "/studies/{uuid}/districts", @@ -422,7 +424,8 @@ def create_district( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.district_manager.create_district(study, dto) + study_interface = study_service.get_study_interface(study) + return study_service.district_manager.create_district(study_interface, dto) @bp.put( "/studies/{uuid}/districts/{district_id}", @@ -441,7 +444,8 @@ def update_district( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.district_manager.update_district(study, district_id, dto) + study_interface = study_service.get_study_interface(study) + study_service.district_manager.update_district(study_interface, district_id, dto) @bp.delete( "/studies/{uuid}/districts/{district_id}", @@ -459,7 +463,8 @@ def remove_district( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.district_manager.remove_district(study, district_id) + study_interface = study_service.get_study_interface(study) + study_service.district_manager.remove_district(study_interface, district_id) @bp.get( "/studies/{uuid}/areas/{area_id}/hydro/form", @@ -479,8 +484,8 @@ def get_hydro_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.hydro_manager.get_field_values(study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.hydro_manager.get_field_values(study_interface, area_id) @bp.put( "/studies/{uuid}/areas/{area_id}/hydro/form", @@ -499,8 +504,8 @@ def set_hydro_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - - study_service.hydro_manager.set_field_values(study, data, area_id) + study_interface = study_service.get_study_interface(study) + study_service.hydro_manager.set_field_values(study_interface, data, area_id) # noinspection SpellCheckingInspection @bp.get( @@ -521,7 +526,8 @@ def get_inflow_structure( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.hydro_manager.get_inflow_structure(study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.hydro_manager.get_inflow_structure(study_interface, area_id) @bp.put( "/studies/{uuid}/areas/{area_id}/hydro/inflow-structure", @@ -541,7 +547,8 @@ def update_inflow_structure( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.hydro_manager.update_inflow_structure(study, area_id, values) + study_interface = study_service.get_study_interface(study) + study_service.hydro_manager.update_inflow_structure(study_interface, area_id, values) @bp.put( "/studies/{uuid}/matrix", @@ -586,7 +593,8 @@ def get_thematic_trimming( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.thematic_trimming_manager.get_field_values(study) + study_interface = study_service.get_study_interface(study) + return study_service.thematic_trimming_manager.get_field_values(study_interface) @bp.put( path="/studies/{uuid}/config/thematictrimming/form", @@ -604,7 +612,8 @@ def set_thematic_trimming( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.thematic_trimming_manager.set_field_values(study, field_values) + study_interface = study_service.get_study_interface(study) + study_service.thematic_trimming_manager.set_field_values(study_interface, field_values) @bp.get( path="/studies/{uuid}/config/playlist/form", @@ -623,8 +632,8 @@ def get_playlist( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.playlist_manager.get_table_data(study) + study_interface = study_service.get_study_interface(study) + return study_service.playlist_manager.get_table_data(study_interface) @bp.put( path="/studies/{uuid}/config/playlist/form", @@ -642,7 +651,8 @@ def set_playlist( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.playlist_manager.set_table_data(study, data) + study_interface = study_service.get_study_interface(study) + study_service.playlist_manager.set_table_data(study_interface, data) @bp.get( "/studies/{uuid}/config/playlist", @@ -660,7 +670,8 @@ def get_playlist_config( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.config_manager.get_playlist(study) + study_interface = study_service.get_study_interface(study) + return study_service.config_manager.get_playlist(study_interface) @bp.put( path="/studies/{uuid}/config/playlist", @@ -681,7 +692,8 @@ def set_playlist_config( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.config_manager.set_playlist(study, playlist, weights, reverse, active) + study_interface = study_service.get_study_interface(study) + study_service.config_manager.set_playlist(study_interface, playlist, weights, reverse, active) @bp.get( path="/studies/{uuid}/config/scenariobuilder", @@ -699,8 +711,8 @@ def get_scenario_builder_config( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.scenario_builder_manager.get_config(study) + study_interface = study_service.get_study_interface(study) + return study_service.scenario_builder_manager.get_config(study_interface) @bp.get( path="/studies/{uuid}/config/scenariobuilder/{scenario_type}", @@ -781,7 +793,8 @@ def get_scenario_builder_config_by_type( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - table_form = study_service.scenario_builder_manager.get_scenario_by_type(study, scenario_type) + study_interface = study_service.get_study_interface(study) + table_form = study_service.scenario_builder_manager.get_scenario_by_type(study_interface, scenario_type) return {scenario_type: table_form} @bp.put( @@ -800,7 +813,8 @@ def update_scenario_builder_config( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.scenario_builder_manager.update_config(study, data) + study_interface = study_service.get_study_interface(study) + study_service.scenario_builder_manager.update_config(study_interface, data) @bp.put( path="/studies/{uuid}/config/scenariobuilder/{scenario_type}", @@ -843,8 +857,11 @@ def update_scenario_builder_config_by_type( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) + study_interface = study_service.get_study_interface(study) table_form = data[scenario_type] - table_form = study_service.scenario_builder_manager.update_scenario_by_type(study, table_form, scenario_type) + table_form = study_service.scenario_builder_manager.update_scenario_by_type( + study_interface, table_form, scenario_type + ) return {scenario_type: table_form} @bp.get( @@ -864,8 +881,8 @@ def get_general_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.general_manager.get_field_values(study) + study_interface = study_service.get_study_interface(study) + return study_service.general_manager.get_field_values(study_interface) @bp.put( path="/studies/{uuid}/config/general/form", @@ -883,8 +900,8 @@ def set_general_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - - study_service.general_manager.set_field_values(study, field_values) + study_interface = study_service.get_study_interface(study) + study_service.general_manager.set_field_values(study_interface, field_values) @bp.get( path="/studies/{uuid}/config/optimization/form", @@ -903,8 +920,8 @@ def get_optimization_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.optimization_manager.get_field_values(study) + study_interface = study_service.get_study_interface(study) + return study_service.optimization_manager.get_field_values(study_interface) @bp.put( path="/studies/{uuid}/config/optimization/form", @@ -922,8 +939,8 @@ def set_optimization_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - - study_service.optimization_manager.set_field_values(study, field_values) + study_interface = study_service.get_study_interface(study) + study_service.optimization_manager.set_field_values(study_interface, field_values) @bp.get( path="/studies/{uuid}/config/adequacypatch/form", @@ -942,8 +959,8 @@ def get_adequacy_patch_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.adequacy_patch_manager.get_field_values(study) + study_interface = study_service.get_study_interface(study) + return study_service.adequacy_patch_manager.get_field_values(study_interface) @bp.put( path="/studies/{uuid}/config/adequacypatch/form", @@ -961,8 +978,8 @@ def set_adequacy_patch_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - - study_service.adequacy_patch_manager.set_field_values(study, field_values) + study_interface = study_service.get_study_interface(study) + study_service.adequacy_patch_manager.set_field_values(study_interface, field_values) @bp.get( path="/studies/{uuid}/timeseries/config", @@ -981,8 +998,8 @@ def get_timeseries_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.ts_config_manager.get_values(study) + study_interface = study_service.get_study_interface(study) + return study_service.ts_config_manager.get_values(study_interface) @bp.put( path="/studies/{uuid}/timeseries/config", @@ -998,7 +1015,8 @@ def set_ts_generation_config( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.ts_config_manager.set_values(study, field_values) + study_interface = study_service.get_study_interface(study) + study_service.ts_config_manager.set_values(study_interface, field_values) @bp.get( path="/table-schema/{table_type}", @@ -1043,8 +1061,9 @@ def get_table_mode( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) + study_interface = study_service.get_study_interface(study) column_list = columns.split(",") if columns else [] - table_data = study_service.table_mode_manager.get_table_data(study, table_type, column_list) + table_data = study_service.table_mode_manager.get_table_data(study_interface, table_type, column_list) return table_data @bp.put( @@ -1088,7 +1107,8 @@ def update_table_mode( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - table_data = study_service.table_mode_manager.update_table_data(study, table_type, data) + study_interface = study_service.get_study_interface(study) + table_data = study_service.table_mode_manager.update_table_data(study_interface, table_type, data) return table_data @bp.post( @@ -1147,6 +1167,7 @@ def get_binding_constraint_list( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) + study_interface = study_service.get_study_interface(study) filters = ConstraintFilters( enabled=enabled, operator=operator, @@ -1158,7 +1179,7 @@ def get_binding_constraint_list( link_id=link_id, cluster_id=cluster_id, ) - return study_service.binding_constraint_manager.get_binding_constraints(study, filters) + return study_service.binding_constraint_manager.get_binding_constraints(study_interface, filters) @bp.get( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}", @@ -1177,7 +1198,8 @@ def get_binding_constraint( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.binding_constraint_manager.get_binding_constraint(study, binding_constraint_id) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.get_binding_constraint(study_interface, binding_constraint_id) @bp.put( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}", @@ -1196,7 +1218,10 @@ def update_binding_constraint( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.binding_constraint_manager.update_binding_constraint(study, binding_constraint_id, data) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.update_binding_constraint( + study_interface, binding_constraint_id, data + ) @bp.get( "/studies/{uuid}/constraint-groups", @@ -1222,7 +1247,8 @@ def get_grouped_constraints( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - result = study_service.binding_constraint_manager.get_grouped_constraints(study) + study_interface = study_service.get_study_interface(study) + result = study_service.binding_constraint_manager.get_grouped_constraints(study_interface) return result @bp.get( @@ -1255,7 +1281,8 @@ def validate_constraint_groups( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.binding_constraint_manager.validate_constraint_groups(study) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.validate_constraint_groups(study_interface) @bp.get( "/studies/{uuid}/constraint-groups/{group}", @@ -1286,7 +1313,8 @@ def get_constraints_by_group( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - result = study_service.binding_constraint_manager.get_constraints_by_group(study, group) + study_interface = study_service.get_study_interface(study) + result = study_service.binding_constraint_manager.get_constraints_by_group(study_interface, group) return result @bp.get( @@ -1321,7 +1349,8 @@ def validate_constraint_group( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.binding_constraint_manager.validate_constraint_group(study, group) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.validate_constraint_group(study_interface, group) @bp.post("/studies/{uuid}/bindingconstraints", tags=[APITag.study_data], summary="Create a binding constraint") def create_binding_constraint( @@ -1335,7 +1364,8 @@ def create_binding_constraint( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.binding_constraint_manager.create_binding_constraint(study, data) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.create_binding_constraint(study_interface, data) @bp.post( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}", @@ -1354,8 +1384,9 @@ def duplicate_binding_constraint( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) + study_interface = study_service.get_study_interface(study) return study_service.binding_constraint_manager.duplicate_binding_constraint( - study, binding_constraint_id, new_constraint_name + study_interface, binding_constraint_id, new_constraint_name ) @bp.delete( @@ -1373,7 +1404,10 @@ def delete_binding_constraint( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.binding_constraint_manager.remove_binding_constraint(study, binding_constraint_id) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.remove_binding_constraint( + study_interface, binding_constraint_id + ) @bp.delete( "/studies/{uuid}/bindingconstraints", @@ -1390,8 +1424,9 @@ def delete_multiple_binding_constraints( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) + study_interface = study_service.get_study_interface(study) return study_service.binding_constraint_manager.remove_multiple_binding_constraints( - study, binding_constraints_ids + study_interface, binding_constraints_ids ) @bp.post( @@ -1419,7 +1454,10 @@ def add_constraint_term( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.binding_constraint_manager.create_constraint_terms(study, binding_constraint_id, [term]) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.create_constraint_terms( + study_interface, binding_constraint_id, [term] + ) @bp.post( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}/terms", @@ -1446,7 +1484,10 @@ def add_constraint_terms( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.binding_constraint_manager.create_constraint_terms(study, binding_constraint_id, terms) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.create_constraint_terms( + study_interface, binding_constraint_id, terms + ) @bp.put( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}/term", @@ -1473,7 +1514,10 @@ def update_constraint_term( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.binding_constraint_manager.update_constraint_terms(study, binding_constraint_id, [term]) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.update_constraint_terms( + study_interface, binding_constraint_id, [term] + ) @bp.put( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}/terms", @@ -1500,7 +1544,10 @@ def update_constraint_terms( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.binding_constraint_manager.update_constraint_terms(study, binding_constraint_id, terms) + study_interface = study_service.get_study_interface(study) + return study_service.binding_constraint_manager.update_constraint_terms( + study_interface, binding_constraint_id, terms + ) @bp.delete( "/studies/{uuid}/bindingconstraints/{binding_constraint_id}/term/{term_id}", @@ -1519,7 +1566,8 @@ def remove_constraint_term( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.binding_constraint_manager.remove_constraint_term(study, binding_constraint_id, term_id) + study_interface = study_service.get_study_interface(study) + study_service.binding_constraint_manager.remove_constraint_term(study_interface, binding_constraint_id, term_id) @bp.get( path="/studies/{uuid}/areas/hydro/allocation/matrix", @@ -1548,7 +1596,8 @@ def get_allocation_matrix( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - return study_service.allocation_manager.get_allocation_matrix(study, all_areas) + study_interface = study_service.get_study_interface(study) + return study_service.allocation_manager.get_allocation_matrix(study_interface, all_areas) @bp.get( path="/studies/{uuid}/areas/{area_id}/hydro/allocation/form", @@ -1576,7 +1625,8 @@ def get_allocation_form_fields( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - return study_service.allocation_manager.get_allocation_form_fields(all_areas, study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.allocation_manager.get_allocation_form_fields(all_areas, study_interface, area_id) @bp.put( path="/studies/{uuid}/areas/{area_id}/hydro/allocation/form", @@ -1614,7 +1664,8 @@ def set_allocation_form_fields( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - return study_service.allocation_manager.set_allocation_form_fields(all_areas, study, area_id, data) + study_interface = study_service.get_study_interface(study) + return study_service.allocation_manager.set_allocation_form_fields(all_areas, study_interface, area_id, data) @bp.get( path="/studies/{uuid}/areas/hydro/correlation/matrix", @@ -1664,10 +1715,10 @@ def get_correlation_matrix( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - manager = CorrelationManager(study_service.storage_service) - return manager.get_correlation_matrix( + study_interface = study_service.get_study_interface(study) + return study_service.correlation_manager.get_correlation_matrix( all_areas, - study, + study_interface, columns.split(",") if columns else [], ) @@ -1712,8 +1763,8 @@ def set_correlation_matrix( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - manager = CorrelationManager(study_service.storage_service) - return manager.set_correlation_matrix(all_areas, study, matrix) + study_interface = study_service.get_study_interface(study) + return study_service.correlation_manager.set_correlation_matrix(all_areas, study_interface, matrix) @bp.get( path="/studies/{uuid}/areas/{area_id}/hydro/correlation/form", @@ -1741,8 +1792,8 @@ def get_correlation_form_fields( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - manager = CorrelationManager(study_service.storage_service) - return manager.get_correlation_form_fields(all_areas, study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.correlation_manager.get_correlation_form_fields(all_areas, study_interface, area_id) @bp.put( path="/studies/{uuid}/areas/{area_id}/hydro/correlation/form", @@ -1780,8 +1831,8 @@ def set_correlation_form_fields( List[AreaInfoDTO], # because `ui=False` study_service.get_all_areas(uuid, area_type=AreaType.AREA, ui=False, params=params), ) - manager = CorrelationManager(study_service.storage_service) - return manager.set_correlation_form_fields(all_areas, study, area_id, data) + study_interface = study_service.get_study_interface(study) + return study_service.correlation_manager.set_correlation_form_fields(all_areas, study_interface, area_id, data) @bp.get( path="/studies/{uuid}/config/advancedparameters/form", @@ -1801,8 +1852,8 @@ def get_advanced_parameters( params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.advanced_parameters_manager.get_field_values(study) + study_interface = study_service.get_study_interface(study) + return study_service.advanced_parameters_manager.get_field_values(study_interface) @bp.put( path="/studies/{uuid}/config/advancedparameters/form", @@ -1820,8 +1871,8 @@ def set_advanced_parameters( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - - study_service.advanced_parameters_manager.set_field_values(study, field_values) + study_interface = study_service.get_study_interface(study) + study_service.advanced_parameters_manager.set_field_values(study_interface, field_values) @bp.put( "/studies/{uuid}/timeseries/generate", @@ -1844,7 +1895,6 @@ def generate_timeseries( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.generate_timeseries(study, params) @bp.get( @@ -1867,8 +1917,8 @@ def get_properties_form_values( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - - return study_service.properties_manager.get_field_values(study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.properties_manager.get_field_values(study_interface, area_id) @bp.put( path="/studies/{uuid}/areas/{area_id}/properties/form", @@ -1889,8 +1939,8 @@ def set_properties_form_values( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - - study_service.properties_manager.set_field_values(study, area_id, form_fields) + study_interface = study_service.get_study_interface(study) + study_service.properties_manager.set_field_values(study_interface, area_id, form_fields) @bp.get( path="/studies/{uuid}/areas/{area_id}/clusters/renewable", @@ -1911,7 +1961,8 @@ def get_renewable_clusters( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.renewable_manager.get_clusters(study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.renewable_manager.get_clusters(study_interface, area_id) @bp.get( path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}", @@ -1933,7 +1984,8 @@ def get_renewable_cluster( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.renewable_manager.get_cluster(study, area_id, cluster_id) + study_interface = study_service.get_study_interface(study) + return study_service.renewable_manager.get_cluster(study_interface, area_id, cluster_id) @bp.get( path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}/form", @@ -1978,7 +2030,8 @@ def create_renewable_cluster( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - return study_service.renewable_manager.create_cluster(study, area_id, cluster_data) + study_interface = study_service.get_study_interface(study) + return study_service.renewable_manager.create_cluster(study_interface, area_id, cluster_data) @bp.patch( path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}", @@ -1999,7 +2052,8 @@ def update_renewable_cluster( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - return study_service.renewable_manager.update_cluster(study, area_id, cluster_id, cluster_data) + study_interface = study_service.get_study_interface(study) + return study_service.renewable_manager.update_cluster(study_interface, area_id, cluster_id, cluster_data) @bp.put( path="/studies/{uuid}/areas/{area_id}/clusters/renewable/{cluster_id}/form", @@ -2045,7 +2099,8 @@ def delete_renewable_clusters( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - study_service.renewable_manager.delete_clusters(study, area_id, cluster_ids) + study_interface = study_service.get_study_interface(study) + study_service.renewable_manager.delete_clusters(study_interface, area_id, cluster_ids) @bp.get( path="/studies/{uuid}/areas/{area_id}/clusters/thermal", @@ -2075,7 +2130,8 @@ def get_thermal_clusters( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.thermal_manager.get_clusters(study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.thermal_manager.get_clusters(study_interface, area_id) @bp.get( path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}", @@ -2107,7 +2163,8 @@ def get_thermal_cluster( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.thermal_manager.get_cluster(study, area_id, cluster_id) + study_interface = study_service.get_study_interface(study) + return study_service.thermal_manager.get_cluster(study_interface, area_id, cluster_id) @bp.get( path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}/form", @@ -2152,7 +2209,8 @@ def create_thermal_cluster( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - return study_service.thermal_manager.create_cluster(study, area_id, cluster_data) + study_interface = study_service.get_study_interface(study) + return study_service.thermal_manager.create_cluster(study_interface, area_id, cluster_data) @bp.patch( path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}", @@ -2183,7 +2241,8 @@ def update_thermal_cluster( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) - return study_service.thermal_manager.update_cluster(study, area_id, cluster_id, cluster_data) + study_interface = study_service.get_study_interface(study) + return study_service.thermal_manager.update_cluster(study_interface, area_id, cluster_id, cluster_data) @bp.put( path="/studies/{uuid}/areas/{area_id}/clusters/thermal/{cluster_id}/form", @@ -2230,7 +2289,8 @@ def validate_cluster_series( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.thermal_manager.validate_series(study, area_id, cluster_id) + study_interface = study_service.get_study_interface(study) + return study_service.thermal_manager.validate_series(study_interface, area_id, cluster_id) @bp.delete( path="/studies/{uuid}/areas/{area_id}/clusters/thermal", @@ -2260,8 +2320,9 @@ def delete_thermal_clusters( ) request_params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params) + study_interface = study_service.get_study_interface(study) study_service.asserts_no_thermal_in_binding_constraints(study, area_id, cluster_ids) - study_service.thermal_manager.delete_clusters(study, area_id, cluster_ids) + study_service.thermal_manager.delete_clusters(study_interface, area_id, cluster_ids) @bp.get( path="/studies/{uuid}/areas/{area_id}/storages/{storage_id}", @@ -2303,7 +2364,8 @@ def get_st_storage( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.st_storage_manager.get_storage(study, area_id, storage_id) + study_interface = study_service.get_study_interface(study) + return study_service.st_storage_manager.get_storage(study_interface, area_id, storage_id) @bp.get( path="/studies/{uuid}/areas/{area_id}/storages", @@ -2343,7 +2405,8 @@ def get_st_storages( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.st_storage_manager.get_storages(study, area_id) + study_interface = study_service.get_study_interface(study) + return study_service.st_storage_manager.get_storages(study_interface, area_id) @bp.get( path="/studies/{uuid}/areas/{area_id}/storages/{storage_id}/series/{ts_name}", @@ -2381,7 +2444,8 @@ def get_st_storage_matrix( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.st_storage_manager.get_matrix(study, area_id, storage_id, ts_name) + study_interface = study_service.get_study_interface(study) + return study_service.st_storage_manager.get_matrix(study_interface, area_id, storage_id, ts_name) @bp.put( path="/studies/{uuid}/areas/{area_id}/storages/{storage_id}/series/{ts_name}", @@ -2415,7 +2479,8 @@ def update_st_storage_matrix( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.st_storage_manager.update_matrix(study, area_id, storage_id, ts_name, ts) + study_interface = study_service.get_study_interface(study) + study_service.st_storage_manager.update_matrix(study_interface, area_id, storage_id, ts_name, ts) @bp.get( path="/studies/{uuid}/areas/{area_id}/storages/{storage_id}/validate", @@ -2445,7 +2510,8 @@ def validate_st_storage_matrices( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.st_storage_manager.validate_matrices(study, area_id, storage_id) + study_interface = study_service.get_study_interface(study) + return study_service.st_storage_manager.validate_matrices(study_interface, area_id, storage_id) @bp.post( path="/studies/{uuid}/areas/{area_id}/storages", @@ -2496,7 +2562,8 @@ def create_st_storage( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.st_storage_manager.create_storage(study, area_id, form) + study_interface = study_service.get_study_interface(study) + return study_service.st_storage_manager.create_storage(study_interface, area_id, form) @bp.patch( path="/studies/{uuid}/areas/{area_id}/storages/{storage_id}", @@ -2548,7 +2615,8 @@ def update_st_storage( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.st_storage_manager.update_storage(study, area_id, storage_id, form) + study_interface = study_service.get_study_interface(study) + return study_service.st_storage_manager.update_storage(study_interface, area_id, storage_id, form) @bp.delete( path="/studies/{uuid}/areas/{area_id}/storages", @@ -2579,7 +2647,8 @@ def delete_st_storages( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - study_service.st_storage_manager.delete_storages(study, area_id, storage_ids) + study_interface = study_service.get_study_interface(study) + study_service.st_storage_manager.delete_storages(study_interface, area_id, storage_ids) @bp.post( path="/studies/{uuid}/areas/{area_id}/{cluster_type}/{source_cluster_id}", @@ -2603,14 +2672,15 @@ def duplicate_cluster( manager: STStorageManager | RenewableManager | ThermalManager if cluster_type == ClusterType.ST_STORAGES: - manager = STStorageManager(study_service.storage_service) + manager = study_service.st_storage_manager elif cluster_type == ClusterType.RENEWABLES: - manager = RenewableManager(study_service.storage_service) + manager = study_service.renewable_manager elif cluster_type == ClusterType.THERMALS: - manager = ThermalManager(study_service.storage_service) + manager = study_service.thermal_manager else: # pragma: no cover raise NotImplementedError(f"Cluster type {cluster_type} not implemented") - return manager.duplicate_cluster(study, area_id, source_cluster_id, new_cluster_name) + study_interface = study_service.get_study_interface(study) + return manager.duplicate_cluster(study_interface, area_id, source_cluster_id, new_cluster_name) return bp diff --git a/antarest/study/web/xpansion_studies_blueprint.py b/antarest/study/web/xpansion_studies_blueprint.py index 260398a1af..c52eac2247 100644 --- a/antarest/study/web/xpansion_studies_blueprint.py +++ b/antarest/study/web/xpansion_studies_blueprint.py @@ -227,7 +227,8 @@ def add_resource( StudyPermissionType.WRITE, RequestParameters(user=current_user), ) - return study_service.xpansion_manager.add_resource(study, resource_type, [file]) + study_interface = study_service.get_study_interface(study) + return study_service.xpansion_manager.add_resource(study_interface, resource_type, [file]) @bp.delete( "/studies/{uuid}/extensions/xpansion/resources/{resource_type}/{filename}", @@ -249,7 +250,8 @@ def delete_resource( StudyPermissionType.WRITE, RequestParameters(user=current_user), ) - return study_service.xpansion_manager.delete_resource(study, resource_type, filename) + study_interface = study_service.get_study_interface(study) + return study_service.xpansion_manager.delete_resource(study_interface, resource_type, filename) @bp.get( "/studies/{uuid}/extensions/xpansion/resources/{resource_type}/{filename}", @@ -271,7 +273,10 @@ def get_resource_content( StudyPermissionType.READ, RequestParameters(user=current_user), ) - output: JSON | bytes | str = study_service.xpansion_manager.get_resource_content(study, resource_type, filename) + study_interface = study_service.get_study_interface(study) + output: JSON | bytes | str = study_service.xpansion_manager.get_resource_content( + study_interface, resource_type, filename + ) if isinstance(output, bytes): try: @@ -302,6 +307,7 @@ def list_resources( StudyPermissionType.READ, RequestParameters(user=current_user), ) - return study_service.xpansion_manager.list_resources(study, resource_type) + study_interface = study_service.get_study_interface(study) + return study_service.xpansion_manager.list_resources(study_interface, resource_type) return bp diff --git a/tests/conftest_services.py b/tests/conftest_services.py index f5aa965d4e..f5890a6d99 100644 --- a/tests/conftest_services.py +++ b/tests/conftest_services.py @@ -296,7 +296,6 @@ def event_bus_fixture() -> IEventBus: def command_factory_fixture( generator_matrix_constants: GeneratorMatrixConstants, simple_matrix_service: SimpleMatrixService, - patch_service: PatchService, ) -> CommandFactory: """ Fixture that creates a CommandFactory instance with a session-level scope. @@ -304,7 +303,6 @@ def command_factory_fixture( Args: generator_matrix_constants: An instance of the GeneratorMatrixConstants class. simple_matrix_service: An instance of the SimpleMatrixService class. - patch_service: An instance of the PatchService class. Returns: An instance of the CommandFactory class with the provided dependencies. @@ -312,7 +310,6 @@ def command_factory_fixture( return CommandFactory( generator_matrix_constants=generator_matrix_constants, matrix_service=simple_matrix_service, - patch_service=patch_service, ) diff --git a/tests/matrixstore/test_in_memory_matrix_service.py b/tests/matrixstore/test_in_memory_matrix_service.py new file mode 100644 index 0000000000..959df8775e --- /dev/null +++ b/tests/matrixstore/test_in_memory_matrix_service.py @@ -0,0 +1,32 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from antarest.matrixstore.in_memory import InMemorySimpleMatrixService +from antarest.matrixstore.model import MatrixDTO + + +def test_matrix_service(): + service = InMemorySimpleMatrixService() + matrix_id = service.create([[1, 2, 3], [4, 5, 6]]) + assert service.exists(matrix_id) + dto = service.get(matrix_id) + assert dto == MatrixDTO( + id=matrix_id, + data=[[1, 2, 3], [4, 5, 6]], + index=["0", "1"], + columns=["0", "1", "2"], + width=3, + height=2, + created_at=dto.created_at, + ) + service.delete(matrix_id) + assert not service.exists(matrix_id) diff --git a/tests/matrixstore/test_matrix_garbage_collector.py b/tests/matrixstore/test_matrix_garbage_collector.py index 25c347fd9e..6c0ce26470 100644 --- a/tests/matrixstore/test_matrix_garbage_collector.py +++ b/tests/matrixstore/test_matrix_garbage_collector.py @@ -53,7 +53,6 @@ def matrix_garbage_collector(tmp_path: Path): command_factory = CommandFactory( generator_matrix_constants=matrix_constant_generator, matrix_service=Mock(spec=MatrixService), - patch_service=Mock(spec=PatchService), ) study_service = Mock() study_service.storage_service.variant_study_service.command_factory = command_factory diff --git a/tests/storage/business/conftest.py b/tests/storage/business/conftest.py new file mode 100644 index 0000000000..e2af927993 --- /dev/null +++ b/tests/storage/business/conftest.py @@ -0,0 +1,47 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import pytest + +from antarest.matrixstore.in_memory import InMemorySimpleMatrixService +from antarest.matrixstore.service import ISimpleMatrixService +from antarest.study.business.area_management import AreaManager +from antarest.study.business.link_management import LinkManager +from antarest.study.business.xpansion_management import XpansionManager +from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants +from antarest.study.storage.variantstudy.model.command_context import CommandContext + + +@pytest.fixture +def matrix_service() -> ISimpleMatrixService: + return InMemorySimpleMatrixService() + + +@pytest.fixture +def command_context(matrix_service: ISimpleMatrixService) -> CommandContext: + matrix_constants = GeneratorMatrixConstants(matrix_service) + matrix_constants.init_constant_matrices() + return CommandContext(generator_matrix_constants=matrix_constants, matrix_service=matrix_service) + + +@pytest.fixture +def xpansion_manager(command_context: CommandContext) -> XpansionManager: + return XpansionManager(command_context) + + +@pytest.fixture +def area_manager(command_context: CommandContext) -> AreaManager: + return AreaManager(command_context) + + +@pytest.fixture +def link_manager(command_context: CommandContext) -> LinkManager: + return LinkManager(command_context) diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index e71a9113d4..451d386c8e 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -11,45 +11,36 @@ # This file is part of the Antares project. import json -import uuid from pathlib import Path from unittest.mock import Mock from zipfile import ZipFile import pytest -from antarest.core.config import InternalMatrixFormat -from antarest.core.jwt import DEFAULT_ADMIN_USER -from antarest.core.requests import RequestParameters -from antarest.core.utils.fastapi_sqlalchemy import db -from antarest.login.utils import current_user_context -from antarest.matrixstore.repository import MatrixContentRepository -from antarest.matrixstore.service import SimpleMatrixService +from antarest.matrixstore.service import ISimpleMatrixService +from antarest.matrixstore.uri_resolver_service import UriResolverService from antarest.study.business.area_management import AreaCreationDTO, AreaManager, AreaType, UpdateAreaUi from antarest.study.business.link_management import LinkDTO, LinkManager -from antarest.study.business.model.link_model import AssetType, LinkStyle, TransmissionCapacity -from antarest.study.model import Patch, PatchArea, PatchCluster, RawStudy, StudyAdditionalData -from antarest.study.repository import StudyMetadataRepository -from antarest.study.storage.patch_service import PatchService +from antarest.study.business.model.link_model import AssetType, TransmissionCapacity +from antarest.study.business.study_interface import FileStudyInterface, StudyInterface +from antarest.study.model import Patch, PatchArea, PatchCluster from antarest.study.storage.rawstudy.model.filesystem.config.files import build from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, DistrictSet, FileStudyTreeConfig, Link from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalConfig +from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants -from antarest.study.storage.variantstudy.command_factory import CommandFactory -from antarest.study.storage.variantstudy.model.command.common import CommandName, FilteringOptions -from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy -from antarest.study.storage.variantstudy.model.model import CommandDTO -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService -from tests.helpers import with_db_context +from antarest.study.storage.variantstudy.model.command.common import FilteringOptions from tests.storage.business.assets import ASSETS_DIR +@pytest.fixture +def study(empty_study: FileStudy) -> StudyInterface: + return FileStudyInterface(empty_study) + + @pytest.fixture(name="empty_study") -def empty_study_fixture(tmp_path: Path) -> FileStudy: +def empty_study_fixture(tmp_path: Path, matrix_service: ISimpleMatrixService) -> FileStudy: """ Fixture for preparing an empty study in the `tmp_path` based on the "empty_study_810.zip" asset. @@ -66,69 +57,22 @@ def empty_study_fixture(tmp_path: Path) -> FileStudy: with ZipFile(ASSETS_DIR / "empty_study_810.zip") as zip_output: zip_output.extractall(path=study_path) config = build(study_path, study_id) - return FileStudy(config, FileStudyTree(Mock(), config)) + context = ContextServer(matrix_service, UriResolverService(matrix_service)) + return FileStudy(config, FileStudyTree(context, config)) -@pytest.fixture(name="matrix_service") -def matrix_service_fixture(tmp_path: Path) -> SimpleMatrixService: - """ - Fixture for creating a matrix service in the `tmp_path`. - - Args: - tmp_path: The temporary path provided by pytest. - - Returns: - An instance of the `SimpleMatrixService` class representing the matrix service. - """ - matrix_path = tmp_path.joinpath("matrix-store") - matrix_path.mkdir() - matrix_content_repository = MatrixContentRepository(bucket_dir=matrix_path, format=InternalMatrixFormat.TSV) - return SimpleMatrixService(matrix_content_repository=matrix_content_repository) - - -@current_user_context(token=DEFAULT_ADMIN_USER) -@with_db_context -def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): - # Prepare the managers that are used in this UT - raw_study_service = Mock(spec=RawStudyService) - variant_study_service = Mock(spec=VariantStudyService) - storage_service = StudyStorageService(raw_study_service, variant_study_service) - area_manager = AreaManager( - storage_service=storage_service, - repository=StudyMetadataRepository(Mock()), - ) - link_manager = LinkManager(storage_service=storage_service) - - # Check `AreaManager` behaviour with a RAW study - study_id = str(uuid.uuid4()) - # noinspection PyArgumentList - study_version = empty_study.config.version - study = RawStudy( - id=study_id, - path=str(empty_study.config.study_path), - additional_data=StudyAdditionalData(), - version="820", - ) - db.session.add(study) - db.session.commit() - - raw_study_service.get_raw.return_value = empty_study - raw_study_service.cache = Mock() - generator_matrix_constants = GeneratorMatrixConstants(matrix_service) - generator_matrix_constants.init_constant_matrices() - variant_study_service.command_factory = CommandFactory( - generator_matrix_constants, - matrix_service, - patch_service=Mock(spec=PatchService), - ) - assert len(empty_study.config.areas.keys()) == 0 +def test_area_crud( + study: StudyInterface, matrix_service: ISimpleMatrixService, area_manager: AreaManager, link_manager: LinkManager +) -> None: + file_study = study.get_files() + assert len(file_study.config.areas.keys()) == 0 area_manager.create_area(study, AreaCreationDTO(name="test", type=AreaType.AREA)) - assert len(empty_study.config.areas.keys()) == 1 - assert json.loads((empty_study.config.study_path / "patch.json").read_text())["areas"]["test"]["country"] is None + assert len(file_study.config.areas.keys()) == 1 + assert json.loads((file_study.config.study_path / "patch.json").read_text())["areas"]["test"]["country"] is None area_manager.update_area_ui(study, "test", UpdateAreaUi(x=100, y=200, color_rgb=(255, 0, 100)), layer="0") - assert empty_study.tree.get(["input", "areas", "test", "ui", "ui"]) == { + assert file_study.tree.get(["input", "areas", "test", "ui", "ui"]) == { "x": 100, "y": 200, "color_r": 255, @@ -146,166 +90,17 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): area2="test2", ), ) - assert empty_study.config.areas["test"].links.get("test2") is not None + assert file_study.config.areas["test"].links.get("test2") is not None link_manager.delete_link(study, "test", "test2") - assert empty_study.config.areas["test"].links.get("test2") is None + assert file_study.config.areas["test"].links.get("test2") is None area_manager.delete_area(study, "test") area_manager.delete_area(study, "test2") - assert len(empty_study.config.areas.keys()) == 0 - - # Check `AreaManager` behaviour with a variant study - variant_id = str(uuid.uuid4()) - # noinspection PyArgumentList - study = VariantStudy( - id=variant_id, - path=str(empty_study.config.study_path), - additional_data=StudyAdditionalData(), - version="820", - ) - variant_study_service.get_raw.return_value = empty_study - area_manager.create_area( - study, - AreaCreationDTO(name="test", type=AreaType.AREA, metadata=PatchArea(country="FR")), - ) - variant_study_service.append_commands.assert_called_with( - variant_id, - [CommandDTO(action=CommandName.CREATE_AREA.value, args={"area_name": "test"}, study_version=study_version)], - RequestParameters(DEFAULT_ADMIN_USER), - ) - assert (empty_study.config.study_path / "patch.json").exists() - assert json.loads((empty_study.config.study_path / "patch.json").read_text())["areas"]["test"]["country"] == "FR" - - area_manager.update_area_ui(study, "test", UpdateAreaUi(x=100, y=200, color_rgb=(255, 0, 100)), layer="0") - variant_study_service.append_commands.assert_called_with( - variant_id, - [ - CommandDTO( - id=None, - action=CommandName.UPDATE_AREA_UI.value, - args={ - "area_id": "test", - "area_ui": UpdateAreaUi( - x=100, y=200, color_rgb=(255, 0, 100), layer_x={}, layer_y={}, layer_color={} - ), - "layer": "0", - }, - study_version=study_version, - ), - ], - RequestParameters(DEFAULT_ADMIN_USER), - ) - - area_manager.create_area(study, AreaCreationDTO(name="test2", type=AreaType.AREA)) - link_manager.create_link( - study, - LinkDTO( - area1="test", - area2="test2", - ), - ) - variant_study_service.append_commands.assert_called_with( - variant_id, - [ - CommandDTO( - action=CommandName.CREATE_LINK.value, - args={ - "area1": "test", - "area2": "test2", - "parameters": { - "area1": "test", - "area2": "test2", - "hurdles_cost": False, - "loop_flow": False, - "use_phase_shifter": False, - "transmission_capacities": TransmissionCapacity.ENABLED, - "asset_type": AssetType.AC, - "display_comments": True, - "comments": "", - "colorr": 112, - "colorg": 112, - "colorb": 112, - "link_width": 1.0, - "link_style": LinkStyle.PLAIN, - "filter_synthesis": "hourly, daily, weekly, monthly, annual", - "filter_year_by_year": "hourly, daily, weekly, monthly, annual", - }, - }, - study_version=study_version, - ), - ], - RequestParameters(DEFAULT_ADMIN_USER), - ) - - study.version = 810 - link_manager.create_link( - study, - LinkDTO( - area1="test", - area2="test2", - ), - ) - variant_study_service.append_commands.assert_called_with( - variant_id, - [ - CommandDTO( - action=CommandName.CREATE_LINK.value, - args={ - "area1": "test", - "area2": "test2", - "parameters": { - "area1": "test", - "area2": "test2", - "hurdles_cost": False, - "loop_flow": False, - "use_phase_shifter": False, - "transmission_capacities": TransmissionCapacity.ENABLED, - "asset_type": AssetType.AC, - "display_comments": True, - "comments": "", - "colorr": 112, - "colorg": 112, - "colorb": 112, - "link_width": 1.0, - "link_style": LinkStyle.PLAIN, - }, - }, - study_version=study_version, - ), - ], - RequestParameters(DEFAULT_ADMIN_USER), - ) - link_manager.delete_link(study, "test", "test2") - variant_study_service.append_commands.assert_called_with( - variant_id, - [ - CommandDTO( - action=CommandName.REMOVE_LINK.value, - args={"area1": "test", "area2": "test2"}, - study_version=study_version, - ), - ], - RequestParameters(DEFAULT_ADMIN_USER), - ) - area_manager.delete_area(study, "test2") - variant_study_service.append_commands.assert_called_with( - variant_id, - [ - CommandDTO(action=CommandName.REMOVE_AREA.value, args={"id": "test2"}, study_version=study_version), - ], - RequestParameters(DEFAULT_ADMIN_USER), - ) + assert len(file_study.config.areas.keys()) == 0 -def test_get_all_area(): - raw_study_service = Mock(spec=RawStudyService) - area_manager = AreaManager( - storage_service=StudyStorageService(raw_study_service, Mock()), - repository=Mock(spec=StudyMetadataRepository), - ) - link_manager = LinkManager(storage_service=StudyStorageService(raw_study_service, Mock())) +def test_get_all_area(area_manager: AreaManager, link_manager: LinkManager) -> None: - study = RawStudy(version="900") config = FileStudyTreeConfig( study_path=Path("somepath"), path=Path("somepath"), @@ -343,10 +138,10 @@ def test_get_all_area(): sets={"s1": DistrictSet(areas=["a1"])}, ) file_tree_mock = Mock(spec=FileStudyTree, context=Mock(), config=config) - raw_study_service.get_raw.return_value = FileStudy(config=config, tree=file_tree_mock) - area_manager.patch_service = Mock() - area_manager.patch_service.get.return_value = Patch( + study_interface = Mock(spec=StudyInterface) + study_interface.get_files.return_value = FileStudy(config, file_tree_mock) + study_interface.get_patch_data.return_value = Patch( areas={"a1": PatchArea(country="fr")}, thermal_clusters={"a1.a": PatchCluster.model_validate({"code-oi": "1"})}, ) @@ -407,7 +202,7 @@ def test_get_all_area(): "id": "a3", }, ] - areas = area_manager.get_all_areas(study, AreaType.AREA) + areas = area_manager.get_all_areas(study_interface, AreaType.AREA) assert expected_areas == [area.model_dump() for area in areas] expected_clusters = [ @@ -420,7 +215,7 @@ def test_get_all_area(): "id": "s1", } ] - clusters = area_manager.get_all_areas(study, AreaType.DISTRICT) + clusters = area_manager.get_all_areas(study_interface, AreaType.DISTRICT) assert expected_clusters == [area.model_dump() for area in clusters] file_tree_mock.get.side_effect = [{}, {}, {}] @@ -458,7 +253,7 @@ def test_get_all_area(): "id": "s1", }, ] - all_areas = area_manager.get_all_areas(study) + all_areas = area_manager.get_all_areas(study_interface) assert expected_all == [area.model_dump() for area in all_areas] file_tree_mock.get.side_effect = [ @@ -509,7 +304,7 @@ def test_get_all_area(): } }, ] - links = link_manager.get_all_links(study) + links = link_manager.get_all_links(study_interface) assert [ { "area1": "a1", @@ -568,14 +363,8 @@ def test_get_all_area(): ] == [link.model_dump(mode="json") for link in links] -def test_update_area(): - raw_study_service = Mock(spec=RawStudyService) - area_manager = AreaManager( - storage_service=StudyStorageService(raw_study_service, Mock()), - repository=Mock(spec=StudyMetadataRepository), - ) +def test_update_area(area_manager: AreaManager): - study = RawStudy() config = FileStudyTreeConfig( study_path=Path("somepath"), path=Path("somepath"), @@ -601,24 +390,18 @@ def test_update_area(): }, sets={"s1": DistrictSet(areas=["a1"])}, ) - raw_study_service.get_raw.return_value = FileStudy(config=config, tree=FileStudyTree(context=Mock(), config=config)) + study_interface = Mock(spec=StudyInterface) - area_manager.patch_service = Mock() - area_manager.patch_service.get.return_value = Patch(areas={"a1": PatchArea(country="fr")}) + study_interface.get_files.return_value = FileStudy(config=config, tree=FileStudyTree(context=Mock(), config=config)) + study_interface.get_patch_data.return_value = Patch(areas={"a1": PatchArea(country="fr")}) - new_area_info = area_manager.update_area_metadata(study, "a1", PatchArea(country="fr")) + new_area_info = area_manager.update_area_metadata(study_interface, "a1", PatchArea(country="fr")) assert new_area_info.id == "a1" assert new_area_info.metadata.model_dump() == {"country": "fr", "tags": []} -def test_update_clusters(): - raw_study_service = Mock(spec=RawStudyService) - area_manager = AreaManager( - storage_service=StudyStorageService(raw_study_service, Mock()), - repository=Mock(spec=StudyMetadataRepository), - ) +def test_update_clusters(area_manager: AreaManager): - study = RawStudy() config = FileStudyTreeConfig( study_path=Path("somepath"), path=Path("somepath"), @@ -636,10 +419,10 @@ def test_update_clusters(): }, ) file_tree_mock = Mock(spec=FileStudyTree, context=Mock(), config=config) - raw_study_service.get_raw.return_value = FileStudy(config=config, tree=file_tree_mock) + study_interface = Mock(spec=StudyInterface) - area_manager.patch_service = Mock() - area_manager.patch_service.get.return_value = Patch( + study_interface.get_files.return_value = FileStudy(config=config, tree=file_tree_mock) + study_interface.get_patch_data.return_value = Patch( areas={"a1": PatchArea(country="fr")}, thermal_clusters={"a1.a": PatchCluster.model_validate({"code-oi": "1"})}, ) @@ -654,7 +437,7 @@ def test_update_clusters(): } ] - new_area_info = area_manager.update_thermal_cluster_metadata(study, "a1", {"a": PatchCluster(type="a")}) + new_area_info = area_manager.update_thermal_cluster_metadata(study_interface, "a1", {"a": PatchCluster(type="a")}) assert len(new_area_info.thermals) == 1 assert new_area_info.thermals[0].type == "a" assert new_area_info.thermals[0].code_oi is None diff --git a/tests/storage/business/test_config_manager.py b/tests/storage/business/test_config_manager.py index e9cbbf53e2..6249d6d6e3 100644 --- a/tests/storage/business/test_config_manager.py +++ b/tests/storage/business/test_config_manager.py @@ -13,8 +13,11 @@ from pathlib import Path from unittest.mock import Mock +import pytest from antares.study.version import StudyVersion +from antarest.matrixstore.service import ISimpleMatrixService +from antarest.study.business.study_interface import StudyInterface from antarest.study.business.thematic_trimming_field_infos import FIELDS_INFO from antarest.study.business.thematic_trimming_management import ( ThematicTrimmingFormFields, @@ -24,25 +27,16 @@ from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.model.dbmodel import VariantStudy -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService -def test_thematic_trimming_config() -> None: - command_context = CommandContext.model_construct() - command_factory_mock = Mock() - command_factory_mock.command_context = command_context - raw_study_service = Mock(spec=RawStudyService) - variant_study_service = Mock(spec=VariantStudyService, command_factory=command_factory_mock) +def test_thematic_trimming_config(command_context: CommandContext) -> None: thematic_trimming_manager = ThematicTrimmingManager( - storage_service=StudyStorageService(raw_study_service, variant_study_service), + command_context=command_context, ) - study = VariantStudy() config = FileStudyTreeConfig( study_path=Path("somepath"), path=Path("somepath"), @@ -52,7 +46,6 @@ def test_thematic_trimming_config() -> None: sets={}, ) file_tree_mock = Mock(spec=FileStudyTree, context=Mock(), config=config) - variant_study_service.get_raw.return_value = FileStudy(config=config, tree=file_tree_mock) file_tree_mock.get.side_effect = [ # For study version < 800: {}, @@ -66,6 +59,9 @@ def test_thematic_trimming_config() -> None: {"variables selection": {"selected_vars_reset": False, "select_var +": ["CONG. FEE (ALG.)"]}}, ] + study = Mock(StudyInterface) + study.get_files.return_value = FileStudy(config=config, tree=file_tree_mock) + study.version = config.version = 700 actual = thematic_trimming_manager.get_field_values(study) fields_info = get_fields_info(StudyVersion.parse(study.version)) @@ -105,7 +101,7 @@ def test_thematic_trimming_config() -> None: new_config = ThematicTrimmingFormFields(**dict.fromkeys(fields_info, True)) new_config.coal = False thematic_trimming_manager.set_field_values(study, new_config) - assert variant_study_service.append_commands.called_with( + assert study.add_commands.called_with( UpdateConfig( target="settings/generaldata/variables selection", data={"select_var -": [FIELDS_INFO["coal"]["path"]]}, @@ -117,7 +113,7 @@ def test_thematic_trimming_config() -> None: new_config = ThematicTrimmingFormFields(**dict.fromkeys(fields_info, False)) new_config.renw_1 = True thematic_trimming_manager.set_field_values(study, new_config) - assert variant_study_service.append_commands.called_with( + assert study.add_commands.called_with( UpdateConfig( target="settings/generaldata/variables selection", data={ diff --git a/tests/storage/business/test_timeseries_config_manager.py b/tests/storage/business/test_timeseries_config_manager.py index ec19d9493b..177cc9626d 100644 --- a/tests/storage/business/test_timeseries_config_manager.py +++ b/tests/storage/business/test_timeseries_config_manager.py @@ -18,19 +18,16 @@ import pytest +from antarest.study.business.study_interface import FileStudyInterface from antarest.study.business.timeseries_config_management import ( TimeSeriesConfigDTO, TimeSeriesConfigManager, TimeSeriesTypeConfig, ) -from antarest.study.model import RawStudy from antarest.study.storage.rawstudy.model.filesystem.config.files import build from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService -from tests.helpers import with_db_context +from antarest.study.storage.variantstudy.model.command_context import CommandContext @pytest.fixture @@ -44,17 +41,14 @@ def file_study_820(tmpdir: Path) -> FileStudy: return FileStudy(config, FileStudyTree(Mock(), config)) -@with_db_context -def test_nominal_case( - file_study_820: FileStudy, raw_study_service: RawStudyService, variant_study_service: VariantStudyService -): +def test_nominal_case(file_study_820: FileStudy, command_context: CommandContext): # Checks default value assert file_study_820.tree.get(["settings", "generaldata", "general", "nbtimeseriesthermal"]) == 1 + study = FileStudyInterface(file_study_820) + # Prepares the test - storage_service = StudyStorageService(raw_study_service, variant_study_service) - config_manager = TimeSeriesConfigManager(storage_service) - study = RawStudy(id="test", path=str(file_study_820.config.path)) + config_manager = TimeSeriesConfigManager(command_context) # Asserts the get method returns the right value assert config_manager.get_values(study) == TimeSeriesConfigDTO(thermal=TimeSeriesTypeConfig(number=1)) diff --git a/tests/storage/business/test_watcher.py b/tests/storage/business/test_watcher.py index 92e73f4d73..27fe5bb63a 100644 --- a/tests/storage/business/test_watcher.py +++ b/tests/storage/business/test_watcher.py @@ -82,6 +82,7 @@ def build_study_service( raw_study_service=raw_study_service, variant_study_service=variant_study_service, user_service=user_service, + command_context=Mock(), repository=repository, event_bus=Mock(), task_service=task_service, diff --git a/tests/storage/business/test_xpansion_manager.py b/tests/storage/business/test_xpansion_manager.py index 6c4d6b7db5..83e5270484 100644 --- a/tests/storage/business/test_xpansion_manager.py +++ b/tests/storage/business/test_xpansion_manager.py @@ -24,6 +24,11 @@ from antarest.core.exceptions import ChildNotFoundError from antarest.core.model import JSON +from antarest.study.business.area_management import AreaManager +from antarest.study.business.link_management import LinkManager +from antarest.study.business.model.area_model import AreaCreationDTO, AreaType +from antarest.study.business.model.link_model import LinkDTO +from antarest.study.business.study_interface import FileStudyInterface, StudyInterface from antarest.study.business.xpansion_management import ( FileCurrentlyUsedInSettings, LinkNotFound, @@ -36,73 +41,33 @@ XpansionManager, XpansionResourceFileType, ) -from antarest.study.model import RawStudy from antarest.study.storage.rawstudy.model.filesystem.config.files import build from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.model.command.create_area import CreateArea -from antarest.study.storage.variantstudy.model.command.create_link import CreateLink -from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService from tests.storage.business.assets import ASSETS_DIR -def make_empty_study(tmpdir: Path, version: int) -> FileStudy: +def make_empty_study(tmpdir: Path, version: int) -> StudyInterface: study_path = Path(tmpdir / str(uuid.uuid4())) os.mkdir(study_path) with zipfile.ZipFile(ASSETS_DIR / f"empty_study_{version}.zip") as zip_output: zip_output.extractall(path=study_path) config = build(study_path, "1") - return FileStudy(config, FileStudyTree(Mock(), config)) + return FileStudyInterface(FileStudy(config, FileStudyTree(Mock(), config))) -def make_xpansion_manager(empty_study: FileStudy) -> XpansionManager: - raw_study_service = Mock(spec=RawStudyService) - variant_study_service = Mock(spec=VariantStudyService) - xpansion_manager = XpansionManager( - study_storage_service=StudyStorageService(raw_study_service, variant_study_service), - ) - raw_study_service.get_raw.return_value = empty_study - raw_study_service.cache = Mock() - return xpansion_manager - - -def make_areas(empty_study: FileStudy) -> None: - study_version = empty_study.config.version - CreateArea( - area_name="area1", - command_context=Mock(spec=CommandContext, generator_matrix_constants=Mock()), - study_version=study_version, - )._apply_config(empty_study.config) - CreateArea( - area_name="area2", - command_context=Mock(spec=CommandContext, generator_matrix_constants=Mock()), - study_version=study_version, - )._apply_config(empty_study.config) - - -def make_link(empty_study: FileStudy) -> None: - CreateLink( - area1="area1", - area2="area2", - command_context=Mock(spec=CommandContext, generator_matrix_constants=Mock()), - study_version=empty_study.config.version, - )._apply_config(empty_study.config) - - -def make_link_and_areas(empty_study: FileStudy) -> None: - make_areas(empty_study) - make_link(empty_study) - - -def set_up_xpansion_manager(tmp_path: Path) -> t.Tuple[FileStudy, RawStudy, XpansionManager]: - empty_study = make_empty_study(tmp_path, 810) - study = RawStudy(id="1", path=str(empty_study.config.study_path), version="810") - xpansion_manager = make_xpansion_manager(empty_study) - xpansion_manager.create_xpansion_configuration(study) - return empty_study, study, xpansion_manager +def make_areas(area_manager: AreaManager, study: StudyInterface) -> None: + area_manager.create_area(study, AreaCreationDTO(type=AreaType.AREA, name="area1")) + area_manager.create_area(study, AreaCreationDTO(type=AreaType.AREA, name="area2")) + + +def make_link(link_manager: LinkManager, study: StudyInterface) -> None: + link_manager.create_link(study, link_creation_dto=LinkDTO(area1="area1", area2="area2")) + + +@pytest.fixture +def empty_study_810(tmp_path: Path) -> StudyInterface: + return make_empty_study(tmp_path, 810) @pytest.mark.unit_test @@ -134,43 +99,41 @@ def set_up_xpansion_manager(tmp_path: Path) -> t.Tuple[FileStudy, RawStudy, Xpan ), ], ) -def test_create_configuration(tmp_path: Path, version: int, expected_output: JSON) -> None: +def test_create_configuration( + xpansion_manager: XpansionManager, tmp_path: Path, version: int, expected_output: JSON +) -> None: """ Test the creation of a configuration. """ empty_study = make_empty_study(tmp_path, version) - study = RawStudy(id="1", path=str(empty_study.config.study_path), version=str(version)) - xpansion_manager = make_xpansion_manager(empty_study) with pytest.raises(ChildNotFoundError): - empty_study.tree.get(["user", "expansion"], expanded=True, depth=9) + empty_study.get_files().tree.get(["user", "expansion"], expanded=True, depth=9) - xpansion_manager.create_xpansion_configuration(study) + xpansion_manager.create_xpansion_configuration(empty_study) - actual = empty_study.tree.get(["user", "expansion"], expanded=True, depth=9) + actual = empty_study.get_files().tree.get(["user", "expansion"], expanded=True, depth=9) assert actual == expected_output @pytest.mark.unit_test -def test_delete_xpansion_configuration(tmp_path: Path) -> None: +def test_delete_xpansion_configuration(xpansion_manager: XpansionManager, tmp_path: Path) -> None: """ Test the deletion of a configuration. """ - empty_study = make_empty_study(tmp_path, 810) - study = RawStudy(id="1", path=str(empty_study.config.study_path), version="810") - xpansion_manager = make_xpansion_manager(empty_study) + study = make_empty_study(tmp_path, 810) with pytest.raises(ChildNotFoundError): - empty_study.tree.get(["user", "expansion"], expanded=True, depth=9) + study.get_files().tree.get(["user", "expansion"], expanded=True, depth=9) xpansion_manager.create_xpansion_configuration(study) - assert empty_study.tree.get(["user", "expansion"], expanded=True, depth=9) + assert study.get_files().tree.get(["user", "expansion"], expanded=True, depth=9) xpansion_manager.delete_xpansion_configuration(study) with pytest.raises(ChildNotFoundError): - empty_study.tree.get(["user", "expansion"], expanded=True, depth=9) + study.get_files().tree.get(["user", "expansion"], expanded=True, depth=9) @pytest.mark.unit_test @@ -199,14 +162,14 @@ def test_delete_xpansion_configuration(tmp_path: Path) -> None: ], ) @pytest.mark.unit_test -def test_get_xpansion_settings(tmp_path: Path, version: int, expected_output: JSON) -> None: +def test_get_xpansion_settings( + xpansion_manager: XpansionManager, tmp_path: Path, version: int, expected_output: JSON +) -> None: """ Test the retrieval of the xpansion settings. """ - empty_study = make_empty_study(tmp_path, version) - study = RawStudy(id="1", path=str(empty_study.config.study_path), version=str(version)) - xpansion_manager = make_xpansion_manager(empty_study) + study = make_empty_study(tmp_path, version) xpansion_manager.create_xpansion_configuration(study) @@ -215,11 +178,12 @@ def test_get_xpansion_settings(tmp_path: Path, version: int, expected_output: JS @pytest.mark.unit_test -def test_update_xpansion_settings(tmp_path: Path) -> None: +def test_update_xpansion_settings(xpansion_manager: XpansionManager, empty_study_810: StudyInterface) -> None: """ Test the retrieval of the xpansion settings. """ - _, study, xpansion_manager = set_up_xpansion_manager(tmp_path) + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) new_settings_obj = { "optimality_gap": 4.0, @@ -260,10 +224,16 @@ def test_update_xpansion_settings(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_add_candidate(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_add_candidate( + link_manager: LinkManager, + area_manager: AreaManager, + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) - actual = empty_study.tree.get(["user", "expansion", "candidates"]) + actual = study.get_files().tree.get(["user", "expansion", "candidates"]) assert actual == {} new_candidate = XpansionCandidateDTO.model_validate( @@ -287,32 +257,40 @@ def test_add_candidate(tmp_path: Path) -> None: with pytest.raises(KeyError): xpansion_manager.add_candidate(study, new_candidate) - make_areas(empty_study) + make_areas(area_manager, study) with pytest.raises(LinkNotFound): xpansion_manager.add_candidate(study, new_candidate) - make_link(empty_study) + make_link(link_manager, study) xpansion_manager.add_candidate(study, new_candidate) candidates = {"1": new_candidate.model_dump(by_alias=True, exclude_none=True)} - actual = empty_study.tree.get(["user", "expansion", "candidates"]) + actual = study.get_files().tree.get(["user", "expansion", "candidates"]) assert actual == candidates xpansion_manager.add_candidate(study, new_candidate2) candidates["2"] = new_candidate2.model_dump(by_alias=True, exclude_none=True) - actual = empty_study.tree.get(["user", "expansion", "candidates"]) + actual = study.get_files().tree.get(["user", "expansion", "candidates"]) assert actual == candidates @pytest.mark.unit_test -def test_get_candidate(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_candidate( + link_manager: LinkManager, + area_manager: AreaManager, + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) + make_areas(area_manager, study) + make_link(link_manager, study) - assert empty_study.tree.get(["user", "expansion", "candidates"]) == {} + assert study.get_files().tree.get(["user", "expansion", "candidates"]) == {} new_candidate = XpansionCandidateDTO.model_validate( { @@ -332,8 +310,6 @@ def test_get_candidate(tmp_path: Path) -> None: } ) - make_link_and_areas(empty_study) - xpansion_manager.add_candidate(study, new_candidate) xpansion_manager.add_candidate(study, new_candidate2) @@ -342,10 +318,18 @@ def test_get_candidate(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_get_candidates(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_candidates( + link_manager: LinkManager, + area_manager: AreaManager, + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) + make_areas(area_manager, study) + make_link(link_manager, study) - assert empty_study.tree.get(["user", "expansion", "candidates"]) == {} + assert study.get_files().tree.get(["user", "expansion", "candidates"]) == {} new_candidate = XpansionCandidateDTO.model_validate( { @@ -365,8 +349,6 @@ def test_get_candidates(tmp_path: Path) -> None: } ) - make_link_and_areas(empty_study) - xpansion_manager.add_candidate(study, new_candidate) xpansion_manager.add_candidate(study, new_candidate2) @@ -377,12 +359,18 @@ def test_get_candidates(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_update_candidates(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) - - assert empty_study.tree.get(["user", "expansion", "candidates"]) == {} +def test_update_candidates( + link_manager: LinkManager, + area_manager: AreaManager, + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) + make_areas(area_manager, study) + make_link(link_manager, study) - make_link_and_areas(empty_study) + assert study.get_files().tree.get(["user", "expansion", "candidates"]) == {} new_candidate = XpansionCandidateDTO.model_validate( { @@ -408,12 +396,18 @@ def test_update_candidates(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_delete_candidate(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) - - assert empty_study.tree.get(["user", "expansion", "candidates"]) == {} +def test_delete_candidate( + link_manager: LinkManager, + area_manager: AreaManager, + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) + make_areas(area_manager, study) + make_link(link_manager, study) - make_link_and_areas(empty_study) + assert study.get_files().tree.get(["user", "expansion", "candidates"]) == {} new_candidate = XpansionCandidateDTO.model_validate( { @@ -441,8 +435,12 @@ def test_delete_candidate(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_update_constraints(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_update_constraints( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) with pytest.raises(XpansionFileNotFoundError): xpansion_manager.update_xpansion_constraints_settings(study=study, constraints_file_name="non_existent_file") @@ -450,7 +448,7 @@ def test_update_constraints(tmp_path: Path) -> None: with pytest.raises(XpansionFileNotFoundError): xpansion_manager.update_xpansion_constraints_settings(study=study, constraints_file_name="non_existent_file") - empty_study.tree.save({"user": {"expansion": {"constraints": {"constraints.txt": b"0"}}}}) + study.get_files().tree.save({"user": {"expansion": {"constraints": {"constraints.txt": b"0"}}}}) actual_settings = xpansion_manager.update_xpansion_constraints_settings(study, "constraints.txt") assert actual_settings.additional_constraints == "constraints.txt" @@ -460,14 +458,19 @@ def test_update_constraints(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_update_constraints_via_the_front(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) - empty_study.tree.save({"user": {"expansion": {"constraints": {"constraints.txt": b"0"}}}}) +def test_update_constraints_via_the_front( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) + + study.get_files().tree.save({"user": {"expansion": {"constraints": {"constraints.txt": b"0"}}}}) # asserts we can update a field without writing the field additional constraint in the file front_settings = UpdateXpansionSettings(master="relaxed") xpansion_manager.update_xpansion_settings(study, front_settings) - json_content = empty_study.tree.get(["user", "expansion", "settings"]) + json_content = study.get_files().tree.get(["user", "expansion", "settings"]) assert "additional-constraints" not in json_content assert json_content["master"] == "relaxed" @@ -476,27 +479,31 @@ def test_update_constraints_via_the_front(tmp_path: Path) -> None: front_settings = UpdateXpansionSettings.model_validate(new_constraint) actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) assert actual_settings.additional_constraints == "constraints.txt" - json_content = empty_study.tree.get(["user", "expansion", "settings"]) + json_content = study.get_files().tree.get(["user", "expansion", "settings"]) assert json_content["additional-constraints"] == "constraints.txt" # asserts the front-end can unselect this constraint by not filling it front_settings = UpdateXpansionSettings() actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) assert actual_settings.additional_constraints == "" - json_content = empty_study.tree.get(["user", "expansion", "settings"]) + json_content = study.get_files().tree.get(["user", "expansion", "settings"]) assert "additional-constraints" not in json_content @pytest.mark.unit_test -def test_update_weights_via_the_front(tmp_path: Path) -> None: +def test_update_weights_via_the_front( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) # Same test as the one for constraints - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) - empty_study.tree.save({"user": {"expansion": {"weights": {"weights.txt": b"0"}}}}) + study.get_files().tree.save({"user": {"expansion": {"weights": {"weights.txt": b"0"}}}}) # asserts we can update a field without writing the field yearly-weights in the file front_settings = UpdateXpansionSettings(master="relaxed") xpansion_manager.update_xpansion_settings(study, front_settings) - json_content = empty_study.tree.get(["user", "expansion", "settings"]) + json_content = study.get_files().tree.get(["user", "expansion", "settings"]) assert "yearly-weights" not in json_content assert json_content["master"] == "relaxed" @@ -505,20 +512,24 @@ def test_update_weights_via_the_front(tmp_path: Path) -> None: front_settings = UpdateXpansionSettings.model_validate(new_constraint) actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) assert actual_settings.yearly_weights == "weights.txt" - json_content = empty_study.tree.get(["user", "expansion", "settings"]) + json_content = study.get_files().tree.get(["user", "expansion", "settings"]) assert json_content["yearly-weights"] == "weights.txt" # asserts the front-end can unselect this weight by not filling it front_settings = UpdateXpansionSettings() actual_settings = xpansion_manager.update_xpansion_settings(study, front_settings) assert actual_settings.yearly_weights == "" - json_content = empty_study.tree.get(["user", "expansion", "settings"]) + json_content = study.get_files().tree.get(["user", "expansion", "settings"]) assert "yearly-weights" not in json_content @pytest.mark.unit_test -def test_add_resources(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_add_resources( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) filename1 = "constraints1.txt" filename2 = "constraints2.txt" @@ -540,20 +551,20 @@ def test_add_resources(tmp_path: Path) -> None: [UploadFile(filename=filename3, file=io.StringIO(content3))], ) - assert filename1 in empty_study.tree.get(["user", "expansion", "constraints"]) - expected1 = empty_study.tree.get(["user", "expansion", "constraints", filename1]) + assert filename1 in study.get_files().tree.get(["user", "expansion", "constraints"]) + expected1 = study.get_files().tree.get(["user", "expansion", "constraints", filename1]) assert content1.encode() == t.cast(bytes, expected1) - assert filename2 in empty_study.tree.get(["user", "expansion", "constraints"]) - expected2 = empty_study.tree.get(["user", "expansion", "constraints", filename2]) + assert filename2 in study.get_files().tree.get(["user", "expansion", "constraints"]) + expected2 = study.get_files().tree.get(["user", "expansion", "constraints", filename2]) assert content2.encode() == t.cast(bytes, expected2) - assert filename3 in empty_study.tree.get(["user", "expansion", "weights"]) + assert filename3 in study.get_files().tree.get(["user", "expansion", "weights"]) assert { "columns": [0], "data": [[2.0]], "index": [0], - } == empty_study.tree.get(["user", "expansion", "weights", filename3]) + } == study.get_files().tree.get(["user", "expansion", "weights", filename3]) settings = xpansion_manager.get_xpansion_settings(study) settings.yearly_weights = filename3 @@ -570,13 +581,19 @@ def test_add_resources(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_get_single_constraints(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_single_constraints( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) constraints_file_content = b"0" constraints_file_name = "constraints.txt" - empty_study.tree.save({"user": {"expansion": {"constraints": {constraints_file_name: constraints_file_content}}}}) + study.get_files().tree.save( + {"user": {"expansion": {"constraints": {constraints_file_name: constraints_file_content}}}} + ) assert ( xpansion_manager.get_resource_content(study, XpansionResourceFileType.CONSTRAINTS, constraints_file_name) @@ -585,17 +602,25 @@ def test_get_single_constraints(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_get_settings_without_sensitivity(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_settings_without_sensitivity( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) - empty_study.tree.delete(["user", "expansion", "sensitivity"]) + study.get_files().tree.delete(["user", "expansion", "sensitivity"]) # should not fail even if the folder doesn't exist as it's optional xpansion_manager.get_xpansion_settings(study) @pytest.mark.unit_test -def test_get_all_constraints(tmp_path: Path) -> None: - _, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_all_constraints( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) filename1 = "constraints1.txt" filename2 = "constraints2.txt" @@ -616,8 +641,12 @@ def test_get_all_constraints(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_add_capa(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_add_capa( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) filename1 = "capa1.txt" filename2 = "capa2.txt" @@ -631,24 +660,28 @@ def test_add_capa(tmp_path: Path) -> None: xpansion_manager.add_resource(study, XpansionResourceFileType.CAPACITIES, upload_file_list) - assert filename1 in empty_study.tree.get(["user", "expansion", "capa"]) + assert filename1 in study.get_files().tree.get(["user", "expansion", "capa"]) assert { "columns": [0], "data": [[0.0]], "index": [0], - } == empty_study.tree.get(["user", "expansion", "capa", filename1]) + } == study.get_files().tree.get(["user", "expansion", "capa", filename1]) - assert filename2 in empty_study.tree.get(["user", "expansion", "capa"]) + assert filename2 in study.get_files().tree.get(["user", "expansion", "capa"]) assert { "columns": [0], "data": [[1.0]], "index": [0], - } == empty_study.tree.get(["user", "expansion", "capa", filename2]) + } == study.get_files().tree.get(["user", "expansion", "capa", filename2]) @pytest.mark.unit_test -def test_delete_capa(tmp_path: Path) -> None: - empty_study, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_delete_capa( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) filename1 = "capa1.txt" filename2 = "capa2.txt" @@ -664,14 +697,18 @@ def test_delete_capa(tmp_path: Path) -> None: xpansion_manager.delete_resource(study, XpansionResourceFileType.CAPACITIES, filename1) - assert filename1 not in empty_study.tree.get(["user", "expansion", "capa"]) + assert filename1 not in study.get_files().tree.get(["user", "expansion", "capa"]) - assert filename2 in empty_study.tree.get(["user", "expansion", "capa"]) + assert filename2 in study.get_files().tree.get(["user", "expansion", "capa"]) @pytest.mark.unit_test -def test_get_single_capa(tmp_path: Path) -> None: - _, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_single_capa( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) filename1 = "capa1.txt" filename2 = "capa2.txt" @@ -695,8 +732,12 @@ def test_get_single_capa(tmp_path: Path) -> None: @pytest.mark.unit_test -def test_get_all_capa(tmp_path: Path) -> None: - _, study, xpansion_manager = set_up_xpansion_manager(tmp_path) +def test_get_all_capa( + xpansion_manager: XpansionManager, + empty_study_810: StudyInterface, +) -> None: + study = empty_study_810 + xpansion_manager.create_xpansion_configuration(study) filename1 = "capa1.txt" filename2 = "capa2.txt" diff --git a/tests/storage/test_service.py b/tests/storage/test_service.py index 05ce1550a0..59e61417fa 100644 --- a/tests/storage/test_service.py +++ b/tests/storage/test_service.py @@ -103,6 +103,7 @@ def build_study_service( return StudyService( raw_study_service=raw_study_service, variant_study_service=variant_study_service, + command_context=Mock(), user_service=user_service, repository=repository, event_bus=event_bus, diff --git a/tests/study/business/areas/test_st_storage_management.py b/tests/study/business/areas/test_st_storage_management.py index a3dfa00a05..86b4f2205b 100644 --- a/tests/study/business/areas/test_st_storage_management.py +++ b/tests/study/business/areas/test_st_storage_management.py @@ -10,34 +10,25 @@ # # This file is part of the Antares project. -import datetime import io import re import typing as t -import uuid from unittest.mock import Mock import numpy as np import pytest from pydantic import ValidationError -from sqlalchemy.orm.session import Session # type: ignore from antarest.core.exceptions import AreaNotFound, STStorageConfigNotFound, STStorageMatrixNotFound, STStorageNotFound -from antarest.core.model import PublicMode from antarest.core.serde.ini_reader import IniReader -from antarest.login.model import Group, User from antarest.study.business.areas.st_storage_management import STStorageInput, STStorageManager -from antarest.study.model import RawStudy, Study, StudyContentStatus +from antarest.study.business.study_interface import FileStudyInterface, StudyInterface +from antarest.study.model import STUDY_VERSION_8_6 from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageGroup from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.command_factory import CommandFactory -from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService # noinspection SpellCheckingInspection LIST_INI = """ @@ -79,50 +70,23 @@ GEN = np.random.default_rng(1000) -class TestSTStorageManager: - @pytest.fixture(name="study_storage_service") - def study_storage_service(self) -> StudyStorageService: - """Return a mocked StudyStorageService.""" - return Mock( - spec=StudyStorageService, - variant_study_service=Mock( - spec=VariantStudyService, - command_factory=Mock( - spec=CommandFactory, - command_context=Mock(spec=CommandContext), - ), - ), - get_storage=Mock(return_value=Mock(spec=RawStudyService, get_raw=Mock(spec=FileStudy))), - ) +def create_study_interface(tree: FileStudyTree) -> StudyInterface: + """ + Creates a mock study interface which returns the provided study tree. + """ + file_study = Mock(spec=FileStudy) + file_study.tree = tree + study = Mock(StudyInterface) + study.get_files.return_value = file_study + study.version = 860 + return study - # noinspection PyArgumentList - @pytest.fixture(name="study_uuid") - def study_uuid_fixture(self, db_session: Session) -> str: - user = User(id=0, name="admin") - group = Group(id="my-group", name="group") - raw_study = RawStudy( - id=str(uuid.uuid4()), - name="Dummy", - version="860", # version 860 is required for the storage feature - author="John Smith", - created_at=datetime.datetime.now(datetime.timezone.utc), - updated_at=datetime.datetime.now(datetime.timezone.utc), - public_mode=PublicMode.FULL, - owner=user, - groups=[group], - workspace="default", - path="/path/to/study", - content_status=StudyContentStatus.WARNING, - ) - db_session.add(raw_study) - db_session.commit() - return t.cast(str, raw_study.id) + +class TestSTStorageManager: def test_get_all_storages__nominal_case( self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, + st_storage_manager: STStorageManager, ) -> None: """ This unit test is to verify the behavior of the `get_all_storages` @@ -131,18 +95,17 @@ def test_get_all_storages__nominal_case( for each area, based on a specific configuration. """ # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + manager = st_storage_manager # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=ALL_STORAGES), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=ALL_STORAGES), + ) ) # Given the following arguments - manager = STStorageManager(study_storage_service) # run all_storages = manager.get_all_storages_props(study) @@ -194,12 +157,7 @@ def test_get_all_storages__nominal_case( } assert actual == expected - def test_get_all_storages__config_not_found( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_all_storages__config_not_found(self, st_storage_manager: STStorageManager) -> None: """ This test verifies that when the `get_all_storages` method is called with a study and the corresponding configuration is not found @@ -207,52 +165,36 @@ def test_get_all_storages__config_not_found( raises the `STStorageConfigNotFound` exception with the expected error message containing the study ID. """ - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(side_effect=KeyError("Oops!")), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(side_effect=KeyError("Oops!")), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # run with pytest.raises(STStorageConfigNotFound, match="not found"): - manager.get_all_storages_props(study) + st_storage_manager.get_all_storages_props(study) - def test_get_st_storages__nominal_case( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_st_storages__nominal_case(self, st_storage_manager: STStorageManager) -> None: """ This unit test is to verify the behavior of the `get_storages` method in the `STStorageManager` class under nominal conditions. It checks whether the method returns the expected storage list based on a specific configuration. """ - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=LIST_CFG), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=LIST_CFG), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # run - groups = manager.get_storages(study, area_id="West") + groups = st_storage_manager.get_storages(study, area_id="West") # Check actual = [form.model_dump(by_alias=True) for form in groups] @@ -296,12 +238,7 @@ def test_get_st_storages__nominal_case( ] assert actual == expected - def test_get_st_storages__config_not_found( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_st_storages__config_not_found(self, st_storage_manager: STStorageManager) -> None: """ This test verifies that when the `get_storages` method is called with a study and area ID, and the corresponding configuration is not found @@ -309,62 +246,40 @@ def test_get_st_storages__config_not_found( raises the `STStorageConfigNotFound` exception with the expected error message containing the study ID and area ID. """ - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(side_effect=KeyError("Oops!")), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(side_effect=KeyError("Oops!")), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # run with pytest.raises(STStorageConfigNotFound, match="not found") as ctx: - manager.get_storages(study, area_id="West") + st_storage_manager.get_storages(study, area_id="West") # ensure the error message contains at least the study ID and area ID err_msg = str(ctx.value) assert "West" in err_msg - def test_get_st_storage__nominal_case( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_st_storage__nominal_case(self, st_storage_manager: STStorageManager) -> None: """ Test the `get_st_storage` method of the `STStorageManager` class under nominal conditions. This test verifies that the `get_st_storage` method returns the expected storage fields for a specific study, area, and storage ID combination. - - Args: - db_session: A database session fixture. - study_storage_service: A study storage service fixture. - study_uuid: The UUID of the study to be tested. """ - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=LIST_CFG["storage1"]), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=LIST_CFG["storage1"]), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # Run the method being tested - edit_form = manager.get_storage(study, area_id="West", storage_id="storage1") + edit_form = st_storage_manager.get_storage(study, area_id="West", storage_id="storage1") # Assert that the returned storage fields match the expected fields actual = edit_form.model_dump(by_alias=True) @@ -383,42 +298,30 @@ def test_get_st_storage__nominal_case( assert actual == expected # noinspection SpellCheckingInspection - def test_update_storage__nominal_case( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_update_storage__nominal_case(self, st_storage_manager: STStorageManager) -> None: """ Test the `update_st_storage` method of the `STStorageManager` class under nominal conditions. This test verifies that the `update_st_storage` method correctly updates the storage fields for a specific study, area, and storage ID combination. - - Args: - db_session: A database session fixture. - study_storage_service: A study storage service fixture. - study_uuid: The UUID of the study to be tested. """ - - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + manager = st_storage_manager # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) ini_file_node = IniFileNode(context=Mock(), config=Mock()) + file_study = Mock(spec=FileStudy) file_study.tree = Mock( spec=FileStudyTree, get=Mock(return_value=LIST_CFG), get_node=Mock(return_value=ini_file_node), ) - mock_config = Mock(spec=FileStudyTreeConfig, study_id=study.id) + mock_config = Mock(spec=FileStudyTreeConfig, study_id="id", version=STUDY_VERSION_8_6) file_study.config = mock_config + study = FileStudyInterface(file_study) + # Given the following arguments - manager = STStorageManager(study_storage_service) edit_form = STStorageInput(initial_level=0, initial_level_optim=False) # Test behavior for area not in study @@ -467,186 +370,112 @@ def test_update_storage__nominal_case( } assert actual == expected - def test_get_st_storage__config_not_found( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_st_storage__config_not_found(self, st_storage_manager: STStorageManager) -> None: """ Test the `get_st_storage` method of the `STStorageManager` class when the configuration is not found. This test verifies that the `get_st_storage` method raises an `STStorageNotFound` exception when the configuration for the provided study, area, and storage ID combination is not found. - - Args: - db_session: A database session fixture. - study_storage_service: A study storage service fixture. - study_uuid: The UUID of the study to be tested. """ - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(side_effect=KeyError("Oops!")), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(side_effect=KeyError("Oops!")), + ) ) - - # Given the following arguments - manager = STStorageManager(study_storage_service) - # Run the method being tested and expect an exception with pytest.raises(STStorageNotFound, match="not found") as ctx: - manager.get_storage(study, area_id="West", storage_id="storage1") + st_storage_manager.get_storage(study, area_id="West", storage_id="storage1") # ensure the error message contains at least the study ID, area ID and storage ID err_msg = str(ctx.value) assert "storage1" in err_msg - def test_get_matrix__nominal_case( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_matrix__nominal_case(self, st_storage_manager: STStorageManager) -> None: """ Test the `get_matrix` method of the `STStorageManager` class under nominal conditions. This test verifies that the `get_matrix` method returns the expected storage matrix for a specific study, area, storage ID, and Time Series combination. - - Args: - db_session: A database session fixture. - study_storage_service: A study storage service fixture. - study_uuid: The UUID of the study to be tested. """ - - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) array = GEN.random((8760, 1)) * 1000 expected = { "index": list(range(8760)), "columns": [0], "data": array.tolist(), } - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=expected), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=expected), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # Run the method being tested - matrix = manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") + matrix = st_storage_manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") # Assert that the returned storage fields match the expected fields actual = matrix.model_dump(by_alias=True) assert actual == expected - def test_get_matrix__config_not_found( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_matrix__config_not_found(self, st_storage_manager: STStorageManager) -> None: """ Test the `get_matrix` method of the `STStorageManager` class when the time series is not found. This test verifies that the `get_matrix` method raises an `STStorageNotFound` exception when the configuration for the provided study, area, time series, and storage ID combination is not found. - - Args: - db_session: A database session fixture. - study_storage_service: A study storage service fixture. - study_uuid: The UUID of the study to be tested. """ - - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(side_effect=KeyError("Oops!")), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(side_effect=KeyError("Oops!")), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # Run the method being tested and expect an exception with pytest.raises(STStorageMatrixNotFound, match="not found") as ctx: - manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") + st_storage_manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") + # ensure the error message contains at least the study ID, area ID and storage ID err_msg = str(ctx.value) assert "storage1" in err_msg assert "inflows" in err_msg - def test_get_matrix__invalid_matrix( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: + def test_get_matrix__invalid_matrix(self, st_storage_manager: STStorageManager) -> None: """ Test the `get_matrix` method of the `STStorageManager` class when the time series is not found. This test verifies that the `get_matrix` method raises an `STStorageNotFound` exception when the configuration for the provided study, area, time series, and storage ID combination is not found. - - Args: - db_session: A database session fixture. - study_storage_service: A study storage service fixture. - study_uuid: The UUID of the study to be tested. """ - - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - # Prepare the mocks - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) array = GEN.random((365, 1)) * 1000 matrix = { "index": list(range(365)), "columns": [0], "data": array.tolist(), } - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=matrix), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=matrix), + ) ) - # Given the following arguments - manager = STStorageManager(study_storage_service) - # Run the method being tested and expect an exception with pytest.raises( ValidationError, match=re.escape("time series must have shape (8760, 1)"), ): - manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") + st_storage_manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") # noinspection SpellCheckingInspection - def test_validate_matrices__nominal( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_validate_matrices__nominal(self, st_storage_manager: STStorageManager) -> None: # prepare some random matrices, insuring `lower_rule_curve` <= `upper_rule_curve` matrices = { @@ -667,23 +496,20 @@ def tree_get(url: t.Sequence[str], **_: t.Any) -> t.MutableMapping[str, t.Any]: "data": array.tolist(), } - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) + file_study = Mock(spec=FileStudy) file_study.tree = Mock(spec=FileStudyTree, get=tree_get) + study = Mock(StudyInterface) + study.get_files.return_value = file_study + study.version = 860 # Given the following arguments, the validation shouldn't raise any exception - manager = STStorageManager(study_storage_service) - assert manager.validate_matrices(study, area_id="West", storage_id="storage1") + assert st_storage_manager.validate_matrices(study, area_id="West", storage_id="storage1") # noinspection SpellCheckingInspection def test_validate_matrices__out_of_bound( self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, + st_storage_manager: STStorageManager, ) -> None: - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) # prepare some random matrices, insuring `lower_rule_curve` <= `upper_rule_curve` matrices = { @@ -704,18 +530,14 @@ def tree_get(url: t.Sequence[str], **_: t.Any) -> t.MutableMapping[str, t.Any]: "data": array.tolist(), } - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock(spec=FileStudyTree, get=tree_get) - - manager = STStorageManager(study_storage_service) + study = create_study_interface(Mock(spec=FileStudyTree, get=tree_get)) # Run the method being tested and expect an exception with pytest.raises( ValidationError, match=re.escape("4 validation errors"), ) as ctx: - manager.validate_matrices(study, area_id="West", storage_id="storage1") + st_storage_manager.validate_matrices(study, area_id="West", storage_id="storage1") assert ctx.value.error_count() == 4 for error in ctx.value.errors(): assert error["type"] == "value_error" @@ -723,15 +545,7 @@ def tree_get(url: t.Sequence[str], **_: t.Any) -> t.MutableMapping[str, t.Any]: assert error["loc"][0] in ["upper_rule_curve", "lower_rule_curve", "pmax_withdrawal", "pmax_injection"] # noinspection SpellCheckingInspection - def test_validate_matrices__rule_curve( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_uuid: str, - ) -> None: - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) - + def test_validate_matrices__rule_curve(self, st_storage_manager: STStorageManager) -> None: # prepare some random matrices, not respecting `lower_rule_curve` <= `upper_rule_curve` upper_curve = np.zeros((8760, 1)) lower_curve = np.ones((8760, 1)) @@ -753,19 +567,14 @@ def tree_get(url: t.Sequence[str], **_: t.Any) -> t.MutableMapping[str, t.Any]: "data": array.tolist(), } - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock(spec=FileStudyTree, get=tree_get) - - # Given the following arguments - manager = STStorageManager(study_storage_service) + study = create_study_interface(Mock(spec=FileStudyTree, get=tree_get)) # Run the method being tested and expect an exception with pytest.raises( ValidationError, match=re.escape("1 validation error"), ) as ctx: - manager.validate_matrices(study, area_id="West", storage_id="storage1") + st_storage_manager.validate_matrices(study, area_id="West", storage_id="storage1") error = ctx.value.errors()[0] assert error["type"] == "value_error" assert ( diff --git a/tests/study/business/areas/test_thermal_management.py b/tests/study/business/areas/test_thermal_management.py index 07bdb58787..7e2906d0e3 100644 --- a/tests/study/business/areas/test_thermal_management.py +++ b/tests/study/business/areas/test_thermal_management.py @@ -10,28 +10,27 @@ # # This file is part of the Antares project. -import datetime -import re -import shutil -import uuid import zipfile from pathlib import Path import pytest -from sqlalchemy.orm.session import Session # type: ignore +import antarest.study.storage.rawstudy.model.filesystem.config.files from antarest.core.exceptions import CommandApplicationError -from antarest.core.model import PublicMode -from antarest.core.utils.fastapi_sqlalchemy import db -from antarest.login.model import Group, User +from antarest.matrixstore.service import ISimpleMatrixService +from antarest.matrixstore.uri_resolver_service import UriResolverService from antarest.study.business.areas.thermal_management import ThermalClusterCreation, ThermalClusterInput, ThermalManager -from antarest.study.model import RawStudy, Study, StudyAdditionalData, StudyContentStatus +from antarest.study.business.study_interface import FileStudyInterface, StudyInterface from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( LawOption, LocalTSGenerationBehavior, ThermalClusterGroup, ) -from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree +from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants +from antarest.study.storage.variantstudy.model.command_context import CommandContext from tests.study.business.areas.assets import ASSETS_DIR @@ -63,71 +62,38 @@ def test_invalid_type(self): ThermalClusterGroup(123) -@pytest.fixture(name="zip_legacy_path") -def zip_legacy_path_fixture(tmp_path: Path) -> Path: - target_dir = tmp_path.joinpath("resources") - target_dir.mkdir() +@pytest.fixture +def study_path(tmp_path: Path) -> Path: + study_path = tmp_path / "study" + study_path.mkdir() resource_zip = ASSETS_DIR.joinpath("thermal_management/study_legacy.zip") - shutil.copy2(resource_zip, target_dir) - return target_dir.joinpath(resource_zip.name) + with zipfile.ZipFile(resource_zip, "r") as zip_ref: + zip_ref.extractall(study_path) + return study_path -@pytest.fixture(name="metadata_legacy") -def metadata_legacy_fixture(tmp_path: Path, zip_legacy_path: Path) -> RawStudy: - with zipfile.ZipFile(zip_legacy_path, mode="r") as zf: - content = zf.read("study.antares").decode("utf-8") - config = dict(re.findall(r"^(\w+)\s*=\s*(.*?)$", content, flags=re.I | re.M)) +def create_file_study(matrix_service: ISimpleMatrixService, study_id: str, path: Path) -> FileStudy: + context = ContextServer(matrix_service, UriResolverService(matrix_service)) + config = antarest.study.storage.rawstudy.model.filesystem.config.files.build(study_id=study_id, study_path=path) + tree = FileStudyTree(context, config) + return FileStudy(config, tree) - workspace_dir = tmp_path.joinpath("studies") - workspace_dir.mkdir() - # noinspection PyArgumentList,SpellCheckingInspection - metadata = RawStudy( - id=str(uuid.uuid4()), - name=config["caption"], - version=config["version"], - author=config["author"], - created_at=datetime.datetime.fromtimestamp(int(config["created"]), datetime.timezone.utc), - updated_at=datetime.datetime.fromtimestamp(int(config["lastsave"]), datetime.timezone.utc), - public_mode=PublicMode.FULL, - workspace="default", - path=str(workspace_dir.joinpath(config["caption"])), - content_status=StudyContentStatus.VALID, - additional_data=StudyAdditionalData(author=config["author"]), - ) +@pytest.fixture +def manager(matrix_service: ISimpleMatrixService, study_path) -> ThermalManager: + matrix_constants = GeneratorMatrixConstants(matrix_service) + matrix_constants.init_constant_matrices() + return ThermalManager(CommandContext(generator_matrix_constants=matrix_constants, matrix_service=matrix_service)) - return metadata - -# noinspection PyArgumentList -@pytest.fixture(name="study_legacy_uuid") -def study_legacy_uuid_fixture( - zip_legacy_path: Path, - metadata_legacy: RawStudy, - study_storage_service: StudyStorageService, - db_session: Session, -) -> str: - study_id = metadata_legacy.id - metadata_legacy.user = User(id=1, name="admin") - metadata_legacy.groups = [Group(id="my-group", name="group")] - db_session.add(metadata_legacy) - db_session.commit() - - with db_session: - metadata = db_session.query(Study).get(study_id) - with open(zip_legacy_path, mode="rb") as fd: - study_storage_service.raw_study_service.import_study(metadata, fd) - - return study_id +@pytest.fixture +def study_interface(matrix_service: ISimpleMatrixService, study_path) -> StudyInterface: + file_study = create_file_study(matrix_service, study_id="my-study", path=study_path) + return FileStudyInterface(file_study) class TestThermalManager: - def test_get_cluster__study_legacy( - self, - db_session: Session, - study_storage_service: StudyStorageService, - study_legacy_uuid: str, - ): + def test_get_cluster__study_legacy(self, manager: ThermalManager, study_interface: StudyInterface): """ Given a legacy study with a thermal cluster, When we get the cluster, @@ -135,13 +101,9 @@ def test_get_cluster__study_legacy( Every property related to version 860 or above should be None. """ # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_legacy_uuid) - - # Given the following arguments - manager = ThermalManager(study_storage_service) # Run the method being tested - form = manager.get_cluster(study, area_id="north", cluster_id="2 avail and must 1") + form = manager.get_cluster(study_interface, area_id="north", cluster_id="2 avail and must 1") # Assert that the returned fields match the expected fields actual = form.model_dump(by_alias=True) @@ -190,9 +152,8 @@ def test_get_cluster__study_legacy( def test_get_clusters__study_legacy( self, - db_session: Session, - study_storage_service: StudyStorageService, - study_legacy_uuid: str, + manager: ThermalManager, + study_interface: StudyInterface, ): """ Given a legacy study with thermal clusters, @@ -200,14 +161,8 @@ def test_get_clusters__study_legacy( Then we should get all cluster properties with the correct names and IDs. Every property related to version 860 or above should be None. """ - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_legacy_uuid) - - # Given the following arguments - manager = ThermalManager(study_storage_service) - # Run the method being tested - groups = manager.get_clusters(study, area_id="north") + groups = manager.get_clusters(study_interface, area_id="north") # Assert that the returned fields match the expected fields actual = [form.model_dump(by_alias=True) for form in groups] @@ -334,8 +289,8 @@ def test_get_clusters__study_legacy( def test_create_cluster__study_legacy( self, - study_storage_service: StudyStorageService, - study_legacy_uuid: str, + manager: ThermalManager, + study_interface: StudyInterface, ): """ Given a legacy study, @@ -343,154 +298,137 @@ def test_create_cluster__study_legacy( Then we should get the cluster properties with the correct name and ID. Every property related to version 860 or above should be None. """ - with db(): - # The study must be fetched from the database - study: RawStudy = db.session.query(Study).get(study_legacy_uuid) - - # Given the following arguments - manager = ThermalManager(study_storage_service) - - props = dict( - name="New Cluster", - group=ThermalClusterGroup.NUCLEAR, - enabled=True, - unitCount=350, - nominalCapacity=1000, - genTs=LocalTSGenerationBehavior.USE_GLOBAL, - minStablePower=0, - minUpTime=15, - minDownTime=20, - co2=12.59, - ) - cluster_data = ThermalClusterCreation(**props) - form = manager.create_cluster(study, area_id="north", cluster_data=cluster_data) + # Given the following arguments + props = dict( + name="New Cluster", + group=ThermalClusterGroup.NUCLEAR, + enabled=True, + unitCount=350, + nominalCapacity=1000, + genTs=LocalTSGenerationBehavior.USE_GLOBAL, + minStablePower=0, + minUpTime=15, + minDownTime=20, + co2=12.59, + ) + cluster_data = ThermalClusterCreation(**props) + form = manager.create_cluster(study_interface, area_id="north", cluster_data=cluster_data) - # Assert that the returned fields match the expected fields - actual = form.model_dump(by_alias=True) - expected = { - "co2": 12.59, - "enabled": True, - "fixedCost": 0.0, - "genTs": LocalTSGenerationBehavior.USE_GLOBAL, - "group": ThermalClusterGroup.NUCLEAR, - "id": "New Cluster", - "lawForced": LawOption.UNIFORM, - "lawPlanned": LawOption.UNIFORM, - "marginalCost": 0.0, - "marketBidCost": 0.0, - "minDownTime": 20, - "minStablePower": 0.0, - "minUpTime": 15, - "mustRun": False, - "name": "New Cluster", - "nh3": None, - "nmvoc": None, - "nominalCapacity": 1000.0, - "nox": None, - "op1": None, - "op2": None, - "op3": None, - "op4": None, - "op5": None, - "pm10": None, - "pm25": None, - "pm5": None, - "so2": None, - "costGeneration": None, - "efficiency": None, - "variableOMCost": None, - "spinning": 0.0, - "spreadCost": 0.0, - "startupCost": 0.0, - "unitCount": 350, - "volatilityForced": 0.0, - "volatilityPlanned": 0.0, - } - assert actual == expected + # Assert that the returned fields match the expected fields + actual = form.model_dump(by_alias=True) + expected = { + "co2": 12.59, + "enabled": True, + "fixedCost": 0.0, + "genTs": LocalTSGenerationBehavior.USE_GLOBAL, + "group": ThermalClusterGroup.NUCLEAR, + "id": "New Cluster", + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "marketBidCost": 0.0, + "minDownTime": 20, + "minStablePower": 0.0, + "minUpTime": 15, + "mustRun": False, + "name": "New Cluster", + "nh3": None, + "nmvoc": None, + "nominalCapacity": 1000.0, + "nox": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + "pm10": None, + "pm25": None, + "pm5": None, + "so2": None, + "costGeneration": None, + "efficiency": None, + "variableOMCost": None, + "spinning": 0.0, + "spreadCost": 0.0, + "startupCost": 0.0, + "unitCount": 350, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + } + assert actual == expected def test_update_cluster( self, - study_storage_service: StudyStorageService, - study_legacy_uuid: str, + manager: ThermalManager, + study_interface: StudyInterface, ): - with db(): - # The study must be fetched from the database - study: RawStudy = db.session.query(Study).get(study_legacy_uuid) - - # Given the following arguments - manager = ThermalManager(study_storage_service) - # When some properties of the cluster are updated - cluster_data = ThermalClusterInput(name="New name", nominalCapacity=2000) - manager.update_cluster(study, area_id="north", cluster_id="2 avail and must 1", cluster_data=cluster_data) + # When some properties of the cluster are updated + cluster_data = ThermalClusterInput(name="New name", nominalCapacity=2000) + manager.update_cluster( + study_interface, area_id="north", cluster_id="2 avail and must 1", cluster_data=cluster_data + ) - # Assert that the returned fields match the expected fields - form = manager.get_cluster(study, area_id="north", cluster_id="2 avail and must 1") - actual = form.model_dump(by_alias=True) - expected = { - "id": "2 avail and must 1", - "group": ThermalClusterGroup.GAS, - "name": "New name", - "enabled": False, - "unitCount": 100, - "nominalCapacity": 2000.0, - "genTs": LocalTSGenerationBehavior.USE_GLOBAL, - "minStablePower": 0.0, - "minUpTime": 1, - "minDownTime": 1, - "mustRun": True, - "spinning": 0.0, - "volatilityForced": 0.0, - "volatilityPlanned": 0.0, - "lawForced": LawOption.UNIFORM, - "lawPlanned": LawOption.UNIFORM, - "marginalCost": 0.0, - "spreadCost": 0.0, - "fixedCost": 0.0, - "startupCost": 0.0, - "marketBidCost": 0.0, - "co2": 7.0, - # Pollutant values are `None` because they are not defined before version 8.6. - "nh3": None, - "so2": None, - "nox": None, - "pm25": None, - "pm5": None, - "pm10": None, - "nmvoc": None, - "op1": None, - "op2": None, - "op3": None, - "op4": None, - "op5": None, - # These values are also None as they are defined in v8.7+ - "costGeneration": None, - "efficiency": None, - "variableOMCost": None, - } - assert actual == expected + # Assert that the returned fields match the expected fields + form = manager.get_cluster(study_interface, area_id="north", cluster_id="2 avail and must 1") + actual = form.model_dump(by_alias=True) + expected = { + "id": "2 avail and must 1", + "group": ThermalClusterGroup.GAS, + "name": "New name", + "enabled": False, + "unitCount": 100, + "nominalCapacity": 2000.0, + "genTs": LocalTSGenerationBehavior.USE_GLOBAL, + "minStablePower": 0.0, + "minUpTime": 1, + "minDownTime": 1, + "mustRun": True, + "spinning": 0.0, + "volatilityForced": 0.0, + "volatilityPlanned": 0.0, + "lawForced": LawOption.UNIFORM, + "lawPlanned": LawOption.UNIFORM, + "marginalCost": 0.0, + "spreadCost": 0.0, + "fixedCost": 0.0, + "startupCost": 0.0, + "marketBidCost": 0.0, + "co2": 7.0, + # Pollutant values are `None` because they are not defined before version 8.6. + "nh3": None, + "so2": None, + "nox": None, + "pm25": None, + "pm5": None, + "pm10": None, + "nmvoc": None, + "op1": None, + "op2": None, + "op3": None, + "op4": None, + "op5": None, + # These values are also None as they are defined in v8.7+ + "costGeneration": None, + "efficiency": None, + "variableOMCost": None, + } + assert actual == expected def test_delete_clusters( self, - study_storage_service: StudyStorageService, - study_legacy_uuid: str, + manager: ThermalManager, + study_interface: StudyInterface, ): - with db(): - # The study must be fetched from the database - study: RawStudy = db.session.query(Study).get(study_legacy_uuid) + # When the clusters are deleted + manager.delete_clusters(study_interface, area_id="north", cluster_ids=["2 avail and must 1", "on and must 2"]) - # Given the following arguments - manager = ThermalManager(study_storage_service) - - # When the clusters are deleted - manager.delete_clusters(study, area_id="north", cluster_ids=["2 avail and must 1", "on and must 2"]) - - # Assert that the returned fields match the expected fields - groups = manager.get_clusters(study, area_id="north") - actual = [form.id for form in groups] - expected = ["2 avail and must 2"] - assert actual == expected + # Assert that the returned fields match the expected fields + groups = manager.get_clusters(study_interface, area_id="north") + actual = [form.id for form in groups] + expected = ["2 avail and must 2"] + assert actual == expected - # A second attempt should raise an error - with pytest.raises(CommandApplicationError): - manager.delete_clusters(study, area_id="north", cluster_ids=["2 avail and must 1"]) + # A second attempt should raise an error + with pytest.raises(CommandApplicationError): + manager.delete_clusters(study_interface, area_id="north", cluster_ids=["2 avail and must 1"]) diff --git a/tests/study/business/conftest.py b/tests/study/business/conftest.py new file mode 100644 index 0000000000..71bbbc94f1 --- /dev/null +++ b/tests/study/business/conftest.py @@ -0,0 +1,51 @@ +# Copyright (c) 2025, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import pytest + +from antarest.matrixstore.in_memory import InMemorySimpleMatrixService +from antarest.matrixstore.service import ISimpleMatrixService +from antarest.study.business.area_management import AreaManager +from antarest.study.business.areas.st_storage_management import STStorageManager +from antarest.study.business.link_management import LinkManager +from antarest.study.business.xpansion_management import XpansionManager +from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants +from antarest.study.storage.variantstudy.model.command_context import CommandContext + + +@pytest.fixture +def matrix_service() -> ISimpleMatrixService: + return InMemorySimpleMatrixService() + + +@pytest.fixture +def command_context(matrix_service: ISimpleMatrixService) -> CommandContext: + matrix_constants = GeneratorMatrixConstants(matrix_service) + matrix_constants.init_constant_matrices() + return CommandContext(generator_matrix_constants=matrix_constants, matrix_service=matrix_service) + + +@pytest.fixture +def area_manager(command_context: CommandContext) -> AreaManager: + return AreaManager(command_context) + + +@pytest.fixture +def link_manager(command_context: CommandContext) -> LinkManager: + return LinkManager(command_context) + + +@pytest.fixture +def st_storage_manager(command_context: CommandContext) -> STStorageManager: + return STStorageManager(command_context) + + +manager = STStorageManager(command_context) diff --git a/tests/study/business/test_allocation_manager.py b/tests/study/business/test_allocation_manager.py index 855e2382de..3365534089 100644 --- a/tests/study/business/test_allocation_manager.py +++ b/tests/study/business/test_allocation_manager.py @@ -10,16 +10,13 @@ # # This file is part of the Antares project. -import datetime import re -import uuid from unittest.mock import Mock, patch import pytest +from antares.study.version import StudyVersion from antarest.core.exceptions import AllocationDataNotFound, AreaNotFound -from antarest.core.model import PublicMode -from antarest.login.model import Group, User from antarest.study.business.allocation_management import ( AllocationField, AllocationFormFields, @@ -27,16 +24,31 @@ AllocationMatrix, ) from antarest.study.business.area_management import AreaInfoDTO, AreaType -from antarest.study.model import STUDY_VERSION_8_8, RawStudy, Study, StudyContentStatus +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import STUDY_VERSION_8_6, STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.command_factory import CommandFactory from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService + + +def create_study_interface(tree: FileStudyTree, version: StudyVersion = STUDY_VERSION_8_6) -> StudyInterface: + """ + Creates a mock study interface which returns the provided study tree. + """ + file_study = Mock(spec=FileStudy) + file_study.tree = tree + study = Mock(StudyInterface) + study.get_files.return_value = file_study + study.version = version + file_study.config.version = version + return study + + +@pytest.fixture +def manager(command_context: CommandContext) -> AllocationManager: + return AllocationManager(command_context) class TestAllocationField: @@ -171,52 +183,9 @@ def test_validation_matrix_no_non_null_values(self): ) -# noinspection SpellCheckingInspection -EXECUTE_OR_ADD_COMMANDS = "antarest.study.business.allocation_management.execute_or_add_commands" - - class TestAllocationManager: - @pytest.fixture(name="study_storage_service") - def study_storage_service(self) -> StudyStorageService: - """Return a mocked StudyStorageService.""" - return Mock( - spec=StudyStorageService, - variant_study_service=Mock( - spec=VariantStudyService, - command_factory=Mock( - spec=CommandFactory, - command_context=Mock(spec=CommandContext), - ), - ), - get_storage=Mock(return_value=Mock(spec=RawStudyService, get_raw=Mock(spec=FileStudy))), - ) - # noinspection PyArgumentList - @pytest.fixture(name="study_uuid") - def study_uuid_fixture(self, db_session) -> str: - user = User(id=0, name="admin") - group = Group(id="my-group", name="group") - raw_study = RawStudy( - id=str(uuid.uuid4()), - name="Dummy", - version="850", - author="John Smith", - created_at=datetime.datetime.now(datetime.timezone.utc), - updated_at=datetime.datetime.now(datetime.timezone.utc), - public_mode=PublicMode.FULL, - owner=user, - groups=[group], - workspace="default", - path="/path/to/study", - content_status=StudyContentStatus.WARNING, - ) - db_session.add(raw_study) - db_session.commit() - return raw_study.id - - def test_get_allocation_matrix__nominal_case(self, db_session, study_storage_service, study_uuid): - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_get_allocation_matrix__nominal_case(self, manager): # Prepare the mocks allocation_cfg = { @@ -225,11 +194,12 @@ def test_get_allocation_matrix__nominal_case(self, db_session, study_storage_ser "s": {"[allocation]": {"s": 0.1, "n": 0.2, "w": 0.6}}, "w": {"[allocation]": {"w": 1}}, } - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=allocation_cfg), + + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=allocation_cfg), + ) ) # Given the following arguments @@ -239,8 +209,6 @@ def test_get_allocation_matrix__nominal_case(self, db_session, study_storage_ser AreaInfoDTO(id="s", name="South", type=AreaType.AREA), AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] - area_id = "*" # all areas - manager = AllocationManager(study_storage_service) # run matrix = manager.get_allocation_matrix(study, all_areas) @@ -257,17 +225,15 @@ def test_get_allocation_matrix__nominal_case(self, db_session, study_storage_ser ], ) - def test_get_allocation_matrix__no_allocation(self, db_session, study_storage_service, study_uuid): - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_get_allocation_matrix__no_allocation(self, manager): # Prepare the mocks allocation_cfg = {} - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=allocation_cfg), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=allocation_cfg), + ) ) # Given the following arguments @@ -277,26 +243,23 @@ def test_get_allocation_matrix__no_allocation(self, db_session, study_storage_se AreaInfoDTO(id="s", name="South", type=AreaType.AREA), AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] - area_id = "*" - manager = AllocationManager(study_storage_service) with pytest.raises(AllocationDataNotFound) as ctx: manager.get_allocation_matrix(study, all_areas) assert re.fullmatch(r"Allocation data.*is not found", ctx.value.detail) - def test_get_allocation_form_fields__nominal_case(self, db_session, study_storage_service, study_uuid): - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_get_allocation_form_fields__nominal_case(self, manager): allocation_cfg = { "n": {"[allocation]": {"n": 1}}, "e": {"[allocation]": {"e": 3, "s": 1}}, "s": {"[allocation]": {"s": 0.1, "n": 0.2, "w": 0.6}}, "w": {"[allocation]": {"w": 1}}, } - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=allocation_cfg["n"]), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=allocation_cfg["n"]), + ) ) all_areas = [ @@ -307,7 +270,6 @@ def test_get_allocation_form_fields__nominal_case(self, db_session, study_storag ] area_id = "n" - manager = AllocationManager(study_storage_service) fields = manager.get_allocation_form_fields(all_areas=all_areas, study=study, area_id=area_id) @@ -317,14 +279,13 @@ def test_get_allocation_form_fields__nominal_case(self, db_session, study_storag ] assert fields.allocation == expected_allocation - def test_get_allocation_form_fields__no_allocation_data(self, db_session, study_storage_service, study_uuid): - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_get_allocation_form_fields__no_allocation_data(self, manager): allocation_cfg = {"n": {}} - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=allocation_cfg["n"]), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=allocation_cfg["n"]), + ) ) all_areas = [ @@ -332,14 +293,12 @@ def test_get_allocation_form_fields__no_allocation_data(self, db_session, study_ ] area_id = "n" - manager = AllocationManager(study_storage_service) with pytest.raises(AllocationDataNotFound) as ctx: manager.get_allocation_form_fields(all_areas=all_areas, study=study, area_id=area_id) assert "n" in ctx.value.detail - def test_set_allocation_form_fields__nominal_case(self, db_session, study_storage_service, study_uuid): - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_set_allocation_form_fields__nominal_case(self, manager): all_areas = [ AreaInfoDTO(id="n", name="North", type=AreaType.AREA), AreaInfoDTO(id="e", name="East", type=AreaType.AREA), @@ -347,38 +306,39 @@ def test_set_allocation_form_fields__nominal_case(self, db_session, study_storag AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] area_id = "n" - manager = AllocationManager(study_storage_service) - study_storage_service.get_storage(study).get_raw(study).config.version = STUDY_VERSION_8_8 - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - with patch( - "antarest.study.business.allocation_management.AllocationManager.get_allocation_data", - return_value={"e": 0.5, "s": 0.25, "w": 0.25}, - ): - manager.set_allocation_form_fields( - all_areas=all_areas, - study=study, - area_id=area_id, - data=AllocationFormFields.model_construct( - allocation=[ - AllocationField.model_construct(area_id="e", coefficient=0.5), - AllocationField.model_construct(area_id="s", coefficient=0.25), - AllocationField.model_construct(area_id="w", coefficient=0.25), - ], - ), - ) + study = create_study_interface( + Mock( + spec=FileStudyTree, + ), + version=STUDY_VERSION_8_8, + ) + with patch( + "antarest.study.business.allocation_management.AllocationManager.get_allocation_data", + return_value={"e": 0.5, "s": 0.25, "w": 0.25}, + ): + manager.set_allocation_form_fields( + all_areas=all_areas, + study=study, + area_id=area_id, + data=AllocationFormFields.construct( + allocation=[ + AllocationField.construct(area_id="e", coefficient=0.5), + AllocationField.construct(area_id="s", coefficient=0.25), + AllocationField.construct(area_id="w", coefficient=0.25), + ], + ), + ) - assert exe.call_count == 1 - mock_call = exe.mock_calls[0] - actual_study, _, actual_commands, _ = mock_call.args - assert actual_study == study + assert study.add_commands.call_count == 1 + mock_call = study.add_commands.mock_calls[0] + (actual_commands,) = mock_call.args assert len(actual_commands) == 1 cmd: UpdateConfig = actual_commands[0] assert cmd.command_name == CommandName.UPDATE_CONFIG assert cmd.target == f"input/hydro/allocation/{area_id}/[allocation]" assert cmd.data == {"e": 0.5, "s": 0.25, "w": 0.25} - def test_set_allocation_form_fields__no_allocation_data(self, db_session, study_storage_service, study_uuid): - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_set_allocation_form_fields__no_allocation_data(self, manager): all_areas = [ AreaInfoDTO(id="n", name="North", type=AreaType.AREA), @@ -388,31 +348,32 @@ def test_set_allocation_form_fields__no_allocation_data(self, db_session, study_ ] area_id = "n" - manager = AllocationManager(study_storage_service) - study_storage_service.get_storage(study).get_raw(study).config.version = STUDY_VERSION_8_8 - - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - with patch( - "antarest.study.business.allocation_management.AllocationManager.get_allocation_data", - side_effect=AllocationDataNotFound(area_id), - ): - with pytest.raises(AllocationDataNotFound) as ctx: - manager.set_allocation_form_fields( - all_areas=all_areas, - study=study, - area_id=area_id, - data=AllocationFormFields.model_construct( - allocation=[ - AllocationField.model_construct(area_id="e", coefficient=0.5), - AllocationField.model_construct(area_id="s", coefficient=0.25), - AllocationField.model_construct(area_id="w", coefficient=0.25), - ], - ), - ) + study = create_study_interface( + Mock( + spec=FileStudyTree, + ), + version=STUDY_VERSION_8_8, + ) + with patch( + "antarest.study.business.allocation_management.AllocationManager.get_allocation_data", + side_effect=AllocationDataNotFound(area_id), + ): + with pytest.raises(AllocationDataNotFound) as ctx: + manager.set_allocation_form_fields( + all_areas=all_areas, + study=study, + area_id=area_id, + data=AllocationFormFields.model_construct( + allocation=[ + AllocationField.model_construct(area_id="e", coefficient=0.5), + AllocationField.model_construct(area_id="s", coefficient=0.25), + AllocationField.model_construct(area_id="w", coefficient=0.25), + ], + ), + ) assert "n" in ctx.value.detail - def test_set_allocation_form_fields__invalid_area_ids(self, db_session, study_storage_service, study_uuid): - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_set_allocation_form_fields__invalid_area_ids(self, manager): all_areas = [ AreaInfoDTO(id="n", name="North", type=AreaType.AREA), @@ -422,8 +383,6 @@ def test_set_allocation_form_fields__invalid_area_ids(self, db_session, study_st ] area_id = "n" - manager = AllocationManager(study_storage_service) - data = AllocationFormFields.model_construct( allocation=[ AllocationField.model_construct(area_id="e", coefficient=0.5), @@ -433,6 +392,6 @@ def test_set_allocation_form_fields__invalid_area_ids(self, db_session, study_st ) with pytest.raises(AreaNotFound) as ctx: - manager.set_allocation_form_fields(all_areas=all_areas, study=study, area_id=area_id, data=data) + manager.set_allocation_form_fields(all_areas=all_areas, study=Mock(), area_id=area_id, data=data) assert "invalid_area" in ctx.value.detail diff --git a/tests/study/business/test_correlation_manager.py b/tests/study/business/test_correlation_manager.py index 8d949fe95b..fe908007c3 100644 --- a/tests/study/business/test_correlation_manager.py +++ b/tests/study/business/test_correlation_manager.py @@ -10,16 +10,13 @@ # # This file is part of the Antares project. -import datetime -import uuid -from unittest.mock import Mock, patch +from unittest.mock import Mock import numpy as np import pytest +from antares.study.version import StudyVersion from antarest.core.exceptions import AreaNotFound -from antarest.core.model import PublicMode -from antarest.login.model import Group, User from antarest.study.business.area_management import AreaInfoDTO, AreaType from antarest.study.business.correlation_management import ( AreaCoefficientItem, @@ -27,16 +24,18 @@ CorrelationManager, CorrelationMatrix, ) -from antarest.study.model import STUDY_VERSION_8_8, RawStudy, Study, StudyContentStatus +from antarest.study.business.study_interface import StudyInterface +from antarest.study.model import STUDY_VERSION_8_6, STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree -from antarest.study.storage.rawstudy.raw_study_service import RawStudyService -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.command_factory import CommandFactory from antarest.study.storage.variantstudy.model.command.common import CommandName from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService + + +@pytest.fixture +def correlation_manager(command_context: CommandContext) -> CorrelationManager: + return CorrelationManager(command_context) class TestCorrelationField: @@ -152,52 +151,22 @@ def test_validation__matrix_not_symmetric(self): ) -# noinspection SpellCheckingInspection -EXECUTE_OR_ADD_COMMANDS = "antarest.study.business.correlation_management.execute_or_add_commands" +def create_study_interface(tree: FileStudyTree, version: StudyVersion = STUDY_VERSION_8_6) -> StudyInterface: + """ + Creates a mock study interface which returns the provided study tree. + """ + file_study = Mock(spec=FileStudy) + file_study.tree = tree + study = Mock(StudyInterface) + study.get_files.return_value = file_study + study.version = version + file_study.config.version = version + return study class TestCorrelationManager: - @pytest.fixture(name="study_storage_service") - def study_storage_service(self) -> StudyStorageService: - """Return a mocked StudyStorageService.""" - return Mock( - spec=StudyStorageService, - variant_study_service=Mock( - spec=VariantStudyService, - command_factory=Mock( - spec=CommandFactory, - command_context=Mock(spec=CommandContext), - ), - ), - get_storage=Mock(return_value=Mock(spec=RawStudyService, get_raw=Mock(spec=FileStudy))), - ) - # noinspection PyArgumentList - @pytest.fixture(name="study_uuid") - def study_uuid_fixture(self, db_session) -> str: - user = User(id=0, name="admin") - group = Group(id="my-group", name="group") - raw_study = RawStudy( - id=str(uuid.uuid4()), - name="Dummy", - version="850", - author="John Smith", - created_at=datetime.datetime.now(datetime.timezone.utc), - updated_at=datetime.datetime.now(datetime.timezone.utc), - public_mode=PublicMode.FULL, - owner=user, - groups=[group], - workspace="default", - path="/path/to/study", - content_status=StudyContentStatus.WARNING, - ) - db_session.add(raw_study) - db_session.commit() - return raw_study.id - - def test_get_correlation_matrix__nominal_case(self, db_session, study_storage_service, study_uuid): - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_get_correlation_matrix__nominal_case(self, correlation_manager): # Prepare the mocks correlation_cfg = { @@ -208,11 +177,12 @@ def test_get_correlation_matrix__nominal_case(self, db_session, study_storage_se "s%w": 0.6, "w%w": 0.1, } - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=correlation_cfg), + + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=correlation_cfg), + ) ) # Given the following arguments @@ -222,10 +192,9 @@ def test_get_correlation_matrix__nominal_case(self, db_session, study_storage_se AreaInfoDTO(id="s", name="South", type=AreaType.AREA), AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] - manager = CorrelationManager(study_storage_service) # run - matrix = manager.get_correlation_matrix(all_areas=all_areas, study=study, columns=[]) + matrix = correlation_manager.get_correlation_matrix(all_areas=all_areas, study=study, columns=[]) # Check assert matrix == CorrelationMatrix( @@ -239,18 +208,17 @@ def test_get_correlation_matrix__nominal_case(self, db_session, study_storage_se ], ) - def test_get_field_values__nominal_case(self, db_session, study_storage_service, study_uuid): - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_get_field_values__nominal_case(self, correlation_manager): # Prepare the mocks # NOTE: "s%s" value is ignored correlation_cfg = {"s%s": 0.1, "n%s": 0.2, "w%n": 0.6} - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=correlation_cfg), + + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=correlation_cfg), + ) ) # Given the following arguments @@ -261,8 +229,7 @@ def test_get_field_values__nominal_case(self, db_session, study_storage_service, AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] area_id = "s" # South - manager = CorrelationManager(study_storage_service) - fields = manager.get_correlation_form_fields(all_areas=all_areas, study=study, area_id=area_id) + fields = correlation_manager.get_correlation_form_fields(all_areas=all_areas, study=study, area_id=area_id) assert fields == CorrelationFormFields( correlation=[ AreaCoefficientItem(area_id="s", coefficient=100.0), @@ -270,19 +237,17 @@ def test_get_field_values__nominal_case(self, db_session, study_storage_service, ] ) - def test_set_field_values__nominal_case(self, db_session, study_storage_service, study_uuid): - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_set_field_values__nominal_case(self, correlation_manager): # Prepare the mocks: North + South correlation_cfg = {} - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=correlation_cfg), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=correlation_cfg), + ), + version=STUDY_VERSION_8_8, ) - file_study.config.version = STUDY_VERSION_8_8 # Given the following arguments all_areas = [ @@ -292,44 +257,39 @@ def test_set_field_values__nominal_case(self, db_session, study_storage_service, AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] area_id = "s" # South - manager = CorrelationManager(study_storage_service) - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - manager.set_correlation_form_fields( - all_areas=all_areas, - study=study, - area_id=area_id, - data=CorrelationFormFields( - correlation=[ - AreaCoefficientItem(area_id="s", coefficient=100), - AreaCoefficientItem(area_id="e", coefficient=30), - AreaCoefficientItem(area_id="n", coefficient=40), - ] - ), - ) + correlation_manager.set_correlation_form_fields( + all_areas=all_areas, + study=study, + area_id=area_id, + data=CorrelationFormFields( + correlation=[ + AreaCoefficientItem(area_id="s", coefficient=100), + AreaCoefficientItem(area_id="e", coefficient=30), + AreaCoefficientItem(area_id="n", coefficient=40), + ] + ), + ) # check update - assert exe.call_count == 1 - mock_call = exe.mock_calls[0] - # signature: execute_or_add_commands(study, file_study, commands, storage_service) - actual_study, _, actual_cmds, _ = mock_call.args - assert actual_study == study + assert study.add_commands.call_count == 1 + mock_call = study.add_commands.mock_calls[0] + # signature: add_commands(commands) + (actual_cmds,) = mock_call.args assert len(actual_cmds) == 1 cmd: UpdateConfig = actual_cmds[0] assert cmd.command_name == CommandName.UPDATE_CONFIG assert cmd.target == "input/hydro/prepro/correlation/annual" assert cmd.data == {"e%s": 0.3, "n%s": 0.4} - def test_set_field_values__area_not_found(self, db_session, study_storage_service, study_uuid): - # The study must be fetched from the database - study: RawStudy = db_session.query(Study).get(study_uuid) + def test_set_field_values__area_not_found(self, correlation_manager): # Prepare the mocks: North + South correlation_cfg = {} - storage = study_storage_service.get_storage(study) - file_study = storage.get_raw(study) - file_study.tree = Mock( - spec=FileStudyTree, - get=Mock(return_value=correlation_cfg), + study = create_study_interface( + Mock( + spec=FileStudyTree, + get=Mock(return_value=correlation_cfg), + ), ) # Given the following arguments @@ -340,19 +300,17 @@ def test_set_field_values__area_not_found(self, db_session, study_storage_servic AreaInfoDTO(id="w", name="West", type=AreaType.AREA), ] area_id = "n" # South - manager = CorrelationManager(study_storage_service) - - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - with pytest.raises(AreaNotFound) as ctx: - manager.set_correlation_form_fields( - all_areas=all_areas, - study=study, - area_id=area_id, - data=CorrelationFormFields( - correlation=[ - AreaCoefficientItem(area_id="UNKNOWN", coefficient=3.14), - ] - ), - ) - assert "'UNKNOWN'" in ctx.value.detail - exe.assert_not_called() + + with pytest.raises(AreaNotFound) as ctx: + correlation_manager.set_correlation_form_fields( + all_areas=all_areas, + study=study, + area_id=area_id, + data=CorrelationFormFields( + correlation=[ + AreaCoefficientItem(area_id="UNKNOWN", coefficient=3.14), + ] + ), + ) + assert "'UNKNOWN'" in ctx.value.detail + study.add_commands.assert_not_called() diff --git a/tests/study/business/test_district_manager.py b/tests/study/business/test_district_manager.py index 43dc7fa1a1..45ce81d6ae 100644 --- a/tests/study/business/test_district_manager.py +++ b/tests/study/business/test_district_manager.py @@ -9,10 +9,11 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from typing import Optional from unittest.mock import Mock, patch import pytest +from antares.study.version import StudyVersion from antarest.core.exceptions import AreaNotFound, DistrictAlreadyExist, DistrictNotFound from antarest.study.business.district_manager import ( @@ -21,46 +22,48 @@ DistrictManager, DistrictUpdateDTO, ) -from antarest.study.model import STUDY_VERSION_8_8, Study -from antarest.study.storage.rawstudy.model.filesystem.config.model import DistrictSet +from antarest.study.business.study_interface import FileStudyInterface, StudyInterface +from antarest.study.model import STUDY_VERSION_8_6, STUDY_VERSION_8_8, Study +from antarest.study.storage.rawstudy.model.filesystem.config.model import DistrictSet, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree from antarest.study.storage.rawstudy.raw_study_service import RawStudyService from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.command_factory import CommandFactory from antarest.study.storage.variantstudy.model.command.create_district import CreateDistrict from antarest.study.storage.variantstudy.model.command.remove_district import RemoveDistrict from antarest.study.storage.variantstudy.model.command.update_district import UpdateDistrict from antarest.study.storage.variantstudy.model.command_context import CommandContext -from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService # noinspection SpellCheckingInspection EXECUTE_OR_ADD_COMMANDS = "antarest.study.business.district_manager.execute_or_add_commands" -def _check_execute_or_add_commands(patched_func, expected_cls): +def _check_add_commands(patched_func, expected_cls): assert patched_func.call_count == 1 - commands = patched_func.mock_calls[0].args[2] + commands = patched_func.mock_calls[0].args[0] command = commands[0] assert isinstance(command, expected_cls) +@pytest.fixture +def manager(command_context: CommandContext) -> DistrictManager: + return DistrictManager(command_context) + + +def create_study_interface(file_study: FileStudy, version: StudyVersion = STUDY_VERSION_8_6) -> StudyInterface: + """ + Creates a mock study interface which returns the provided study tree. + """ + study = Mock(StudyInterface) + study.get_files.return_value = file_study + study.version = version + file_study.config.version = version + return study + + class TestDistrictManager: - @pytest.fixture(name="study_storage_service") - def study_storage_service(self): - """Return a mocked StudyStorageService for the DistrictManager unit tests.""" - return Mock( - spec=StudyStorageService, - variant_study_service=Mock( - spec=VariantStudyService, - command_factory=Mock( - spec=CommandFactory, - command_context=Mock(spec=CommandContext), - ), - ), - ) - def test_get_districts(self, study_storage_service: StudyStorageService): + def test_get_districts(self, manager: DistrictManager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = { @@ -77,13 +80,9 @@ def test_get_districts(self, study_storage_service: StudyStorageService): config=Mock(areas=areas, sets=sets), tree=file_study_tree, ) - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) actual = manager.get_districts(study) expected = [ DistrictInfoDTO( @@ -110,7 +109,7 @@ def test_get_districts(self, study_storage_service: StudyStorageService): ] assert actual == expected - def test_create_district__district_already_exist(self, study_storage_service: StudyStorageService): + def test_create_district__district_already_exist(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = { @@ -123,18 +122,14 @@ def test_create_district__district_already_exist(self, study_storage_service: St config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) dto = DistrictCreationDTO(name="d1", output=True, comments="", areas=[]) with pytest.raises(DistrictAlreadyExist): manager.create_district(study, dto) - def test_create_district__area_not_found(self, study_storage_service: StudyStorageService): + def test_create_district__area_not_found(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = {} @@ -145,13 +140,9 @@ def test_create_district__area_not_found(self, study_storage_service: StudyStora config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) dto = DistrictCreationDTO( name="d1", output=True, @@ -161,7 +152,7 @@ def test_create_district__area_not_found(self, study_storage_service: StudyStora with pytest.raises(AreaNotFound, match=r"MISSING"): manager.create_district(study, dto) - def test_create_district__nominal(self, study_storage_service: StudyStorageService): + def test_create_district__nominal(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = { @@ -175,21 +166,16 @@ def test_create_district__nominal(self, study_storage_service: StudyStorageServi tree=Mock(spec=FileStudyTree), ) file_study.config.version = STUDY_VERSION_8_8 - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study, version=STUDY_VERSION_8_8) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) dto = DistrictCreationDTO( name="D1", output=True, comments="hello", areas=["n1", "n2", "n2"], # areas can have duplicates ) - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - actual = manager.create_district(study, dto) + actual = manager.create_district(study, dto) expected = DistrictInfoDTO( id="d1", name="D1", @@ -199,9 +185,9 @@ def test_create_district__nominal(self, study_storage_service: StudyStorageServi ) actual.areas.sort() assert actual == expected - _check_execute_or_add_commands(exe, CreateDistrict) + _check_add_commands(study.add_commands, CreateDistrict) - def test_update_district__district_not_found(self, study_storage_service: StudyStorageService): + def test_update_district__district_not_found(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = {} @@ -212,18 +198,14 @@ def test_update_district__district_not_found(self, study_storage_service: StudyS config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study, version=STUDY_VERSION_8_8) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) dto = DistrictUpdateDTO(output=True, comments="", areas=[]) with pytest.raises(DistrictNotFound, match="MISSING"): manager.update_district(study, "MISSING", dto) - def test_update_district__area_not_found(self, study_storage_service: StudyStorageService): + def test_update_district__area_not_found(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = { @@ -236,13 +218,9 @@ def test_update_district__area_not_found(self, study_storage_service: StudyStora config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study, version=STUDY_VERSION_8_8) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) dto = DistrictUpdateDTO( output=True, comments="", @@ -251,7 +229,7 @@ def test_update_district__area_not_found(self, study_storage_service: StudyStora with pytest.raises(AreaNotFound, match=r"MISSING"): manager.update_district(study, "d1", dto) - def test_update_district__nominal(self, study_storage_service: StudyStorageService): + def test_update_district__nominal(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = { @@ -264,24 +242,18 @@ def test_update_district__nominal(self, study_storage_service: StudyStorageServi config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - file_study.config.version = STUDY_VERSION_8_8 - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study, version=STUDY_VERSION_8_8) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) dto = DistrictUpdateDTO( output=True, comments="", areas=["n2", "n3"], ) - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - manager.update_district(study, "d1", dto) - _check_execute_or_add_commands(exe, UpdateDistrict) + manager.update_district(study, "d1", dto) + _check_add_commands(study.add_commands, UpdateDistrict) - def test_remove_district__district_not_found(self, study_storage_service: StudyStorageService): + def test_remove_district__district_not_found(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = {} @@ -292,17 +264,13 @@ def test_remove_district__district_not_found(self, study_storage_service: StudyS config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study, version=STUDY_VERSION_8_8) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) with pytest.raises(DistrictNotFound, match="MISSING"): manager.remove_district(study, district_id="MISSING") - def test_remove_district__nominal(self, study_storage_service: StudyStorageService): + def test_remove_district__nominal(self, manager): # prepare data areas = dict.fromkeys(["n1", "n2", "n3"]) sets = { @@ -315,14 +283,8 @@ def test_remove_district__nominal(self, study_storage_service: StudyStorageServi config=Mock(areas=areas, sets=sets), tree=Mock(spec=FileStudyTree), ) - file_study.config.version = STUDY_VERSION_8_8 - raw_study_service = Mock(spec=RawStudyService) - raw_study_service.get_raw.return_value = file_study - study_storage_service.get_storage.return_value = raw_study_service + study = create_study_interface(file_study, version=STUDY_VERSION_8_8) # run - manager = DistrictManager(study_storage_service) - study = Mock(spec=Study) - with patch(EXECUTE_OR_ADD_COMMANDS) as exe: - manager.remove_district(study, district_id="d1") - _check_execute_or_add_commands(exe, RemoveDistrict) + manager.remove_district(study, district_id="d1") + _check_add_commands(study.add_commands, RemoveDistrict) diff --git a/tests/variantstudy/conftest.py b/tests/variantstudy/conftest.py index bbd7ee895f..3f99143039 100644 --- a/tests/variantstudy/conftest.py +++ b/tests/variantstudy/conftest.py @@ -30,8 +30,6 @@ from antarest.matrixstore.model import MatrixDTO from antarest.matrixstore.service import MatrixService from antarest.matrixstore.uri_resolver_service import UriResolverService -from antarest.study.repository import StudyMetadataRepository -from antarest.study.storage.patch_service import PatchService from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.context import ContextServer from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy @@ -127,7 +125,6 @@ def command_context_fixture(matrix_service: MatrixService) -> CommandContext: command_context = CommandContext( generator_matrix_constants=generator_matrix_constants, matrix_service=matrix_service, - patch_service=PatchService(repository=Mock(spec=StudyMetadataRepository)), ) return command_context @@ -148,7 +145,6 @@ def command_factory_fixture(matrix_service: MatrixService) -> CommandFactory: return CommandFactory( generator_matrix_constants=generator_matrix_constants, matrix_service=matrix_service, - patch_service=PatchService(), ) diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 699b4bb51c..60696ab84a 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -761,7 +761,6 @@ def get_matrix_id(matrix: str) -> str: return CommandFactory( generator_matrix_constants=Mock(spec=GeneratorMatrixConstants), matrix_service=Mock(spec=MatrixService, get_matrix_id=get_matrix_id), - patch_service=Mock(spec=PatchService), ) @@ -816,7 +815,6 @@ def test_unknown_command(): command_factory = CommandFactory( generator_matrix_constants=Mock(spec=GeneratorMatrixConstants), matrix_service=Mock(spec=MatrixService), - patch_service=Mock(spec=PatchService), ) command_factory.to_command( command_dto=CommandDTO(action="unknown_command", args={}, study_version=STUDY_VERSION_8_8)