From cf4c30672c49697fdefd56e56f486b849f134b00 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Mon, 6 Jan 2025 13:31:06 +0100 Subject: [PATCH 01/14] Revert "fix(lowercase): revert lowercase evolution temporarily (#2280)" This reverts commit e929e3d56215ffa534fcfd38fcf56eef7057e913. --- antarest/core/model.py | 4 + .../extensions/adequacy_patch/extension.py | 2 +- antarest/study/business/area_management.py | 3 +- .../business/areas/renewable_management.py | 30 ++-- .../business/areas/st_storage_management.py | 14 +- .../business/areas/thermal_management.py | 49 +++--- .../business/binding_constraint_management.py | 6 +- antarest/study/business/district_manager.py | 2 +- .../study/business/table_mode_management.py | 7 +- .../model/filesystem/config/cluster.py | 7 +- .../filesystem/config/field_validators.py | 27 +++- .../rawstudy/model/filesystem/config/files.py | 6 +- .../model/filesystem/config/identifier.py | 35 +---- .../rawstudy/model/filesystem/config/model.py | 20 --- .../model/filesystem/config/renewable.py | 44 ++++-- .../model/filesystem/config/st_storage.py | 40 +++-- .../model/filesystem/config/thermal.py | 57 ++++--- .../root/input/thermal/prepro/area/area.py | 5 +- .../business/command_extractor.py | 2 +- .../variantstudy/business/command_reverter.py | 4 +- .../variantstudy/model/command/create_area.py | 8 +- .../command/create_binding_constraint.py | 14 +- .../model/command/create_cluster.py | 86 +++++------ .../model/command/create_district.py | 15 +- .../command/create_renewables_cluster.py | 50 +++---- .../model/command/create_st_storage.py | 50 ++++--- .../model/command/remove_cluster.py | 4 +- .../variantstudy/model/command/remove_link.py | 5 +- .../command/remove_renewables_cluster.py | 4 +- .../model/command/remove_st_storage.py | 3 +- antarest/study/web/study_data_blueprint.py | 12 +- tests/integration/assets/base_study.zip | Bin 305876 -> 331678 bytes tests/integration/assets/variant_study.zip | Bin 311282 -> 309622 bytes .../test_synthesis/raw_study.synthesis.json | 72 ++++----- .../variant_study.synthesis.json | 72 ++++----- .../test_binding_constraints.py | 16 +- .../study_data_blueprint/test_renewable.py | 28 ++-- .../study_data_blueprint/test_st_storage.py | 36 +++-- .../study_data_blueprint/test_table_mode.py | 108 +++++++------- .../study_data_blueprint/test_thermal.py | 45 +++--- tests/integration/test_integration.py | 59 ++++---- .../test_integration_token_end_to_end.py | 18 +-- .../test_renewable_cluster.py | 38 ++--- .../variant_blueprint/test_st_storage.py | 2 +- .../variant_blueprint/test_thermal_cluster.py | 4 +- .../business/test_study_version_upgrader.py | 2 +- .../filesystem/config/test_utils.py | 5 +- .../areas/test_st_storage_management.py | 14 +- .../business/areas/test_thermal_management.py | 6 +- .../rawstudy/test_raw_study_service.py | 21 ++- .../variantstudy/test_snapshot_generator.py | 52 +++++-- .../test_variant_study_service.py | 22 ++- .../model/command/test_create_area.py | 3 +- .../model/command/test_create_cluster.py | 44 +++--- .../model/command/test_create_link.py | 2 +- .../command/test_create_renewables_cluster.py | 37 +++-- .../model/command/test_create_st_storage.py | 139 +++++++----------- .../test_manage_binding_constraints.py | 2 +- .../model/command/test_manage_district.py | 2 +- .../model/command/test_remove_area.py | 2 +- .../model/command/test_remove_cluster.py | 12 +- .../model/command/test_remove_link.py | 4 +- .../command/test_remove_renewables_cluster.py | 5 +- .../model/command/test_remove_st_storage.py | 19 +-- .../model/command/test_replace_matrix.py | 2 +- .../model/command/test_update_config.py | 2 +- tests/variantstudy/test_command_factory.py | 84 +++++------ .../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 +-- 75 files changed, 861 insertions(+), 794 deletions(-) diff --git a/antarest/core/model.py b/antarest/core/model.py index a0c61e9830..2fafaf2b28 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.serialization 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..e61be58703 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.field_validators 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 c607be980d..5eb76faf6e 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -32,7 +32,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.field_validators 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 e82a13f3db..ca70ea8bdf 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -22,12 +22,13 @@ 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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( RenewableConfig, RenewableConfigType, RenewableProperties, create_renewable_config, + create_renewable_properties, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -274,7 +275,6 @@ def update_cluster( Raises: RenewableClusterNotFound: If the cluster to update is not found. """ - study_version = StudyVersion.parse(study.version) file_study = self._get_file_study(study) path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) @@ -284,19 +284,19 @@ def update_cluster( except KeyError: raise RenewableClusterNotFound(path, cluster_id) from None else: - old_config = create_renewable_config(study_version, **values) + old_properties = create_renewable_properties(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_data = new_config.model_dump(mode="json", by_alias=True, exclude={"id"}) + new_properties = old_properties.copy(exclude={"id"}, update=new_values) # create the dict containing the new values using aliases data: t.Dict[str, t.Any] = {} - for field_name, field in new_config.model_fields.items(): - if field_name in new_values: - name = field.alias if field.alias else field_name - data[name] = new_data[name] + for updated_field, updated_value in new_values.items(): + if updated_field in old_properties.model_fields: + field_info = old_properties.model_fields[updated_field] + field_name = field_info.alias if field_info.alias else updated_field + data[field_name] = updated_value # create the update config commands with the modified data command_context = self.storage_service.variant_study_service.command_factory.command_context @@ -308,7 +308,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_properties.model_dump(by_alias=False) return RenewableClusterOutput(**values, id=cluster_id) def delete_clusters(self, study: Study, area_id: str, cluster_ids: t.Sequence[str]) -> None: @@ -357,9 +357,8 @@ def duplicate_cluster( Raises: DuplicateRenewableCluster: If a cluster with the new name already exists in the area. """ - new_id = transform_name_to_id(new_cluster_name, lower=False) - lower_new_id = new_id.lower() - if any(lower_new_id == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): + new_id = transform_name_to_id(new_cluster_name) + if any(new_id == cluster.id for cluster in self.get_clusters(study, area_id)): raise DuplicateRenewableCluster(area_id, new_id) # Cluster duplication @@ -371,9 +370,8 @@ def duplicate_cluster( create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) # Matrix edition - lower_source_id = source_id.lower() - source_path = f"input/renewables/series/{area_id}/{lower_source_id}/series" - new_path = f"input/renewables/series/{area_id}/{lower_new_id}/series" + source_path = f"input/renewables/series/{area_id}/{source_id}/series" + new_path = f"input/renewables/series/{area_id}/{new_id}/series" # Prepare and execute commands storage_service = self.storage_service.get_storage(study) diff --git a/antarest/study/business/areas/st_storage_management.py b/antarest/study/business/areas/st_storage_management.py index ef89e345d8..258b08770f 100644 --- a/antarest/study/business/areas/st_storage_management.py +++ b/antarest/study/business/areas/st_storage_management.py @@ -33,7 +33,7 @@ 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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( STStorage880Config, STStorage880Properties, @@ -305,7 +305,7 @@ def _make_create_cluster_cmd( ) -> CreateSTStorage: command = CreateSTStorage( area_id=area_id, - parameters=cluster, + 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, ) @@ -551,8 +551,7 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus DuplicateSTStorage: If a cluster with the new name already exists in the area. """ new_id = transform_name_to_id(new_cluster_name) - lower_new_id = new_id.lower() - if any(lower_new_id == storage.id.lower() for storage in self.get_storages(study, area_id)): + if any(new_id == storage.id for storage in self.get_storages(study, area_id)): raise DuplicateSTStorage(area_id, new_id) # Cluster duplication @@ -571,16 +570,13 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) # Matrix edition - lower_source_id = source_id.lower() # noinspection SpellCheckingInspection ts_names = ["pmax_injection", "pmax_withdrawal", "lower_rule_curve", "upper_rule_curve", "inflows"] source_paths = [ - _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=lower_source_id, ts_name=ts_name) - for ts_name in ts_names + _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=source_id, ts_name=ts_name) for ts_name in ts_names ] new_paths = [ - _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=lower_new_id, ts_name=ts_name) - for ts_name in ts_names + _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=new_id, ts_name=ts_name) for ts_name in ts_names ] # Prepare and execute commands diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 90c2420881..dba96f570c 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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ( Thermal870Config, Thermal870Properties, ThermalConfigType, create_thermal_config, + create_thermal_properties, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -348,7 +349,6 @@ def update_cluster( ThermalClusterNotFound: If the provided `cluster_id` does not match the ID of the cluster in the provided cluster_data. """ - study_version = StudyVersion.parse(study.version) file_study = self._get_file_study(study) path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) @@ -357,19 +357,19 @@ def update_cluster( except KeyError: raise ThermalClusterNotFound(path, cluster_id) from None else: - old_config = create_thermal_config(study_version, **values) + old_properties = create_thermal_properties(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_data = new_config.model_dump(mode="json", by_alias=True, exclude={"id"}) + new_properties = old_properties.copy(exclude={"id"}, update=new_values) # create the dict containing the new values using aliases data: t.Dict[str, t.Any] = {} - for field_name, field in new_config.model_fields.items(): - if field_name in new_values: - name = field.alias if field.alias else field_name - data[name] = new_data[name] + for updated_field, updated_value in new_values.items(): + if updated_field in old_properties.model_fields: + field_info = old_properties.model_fields[updated_field] + field_name = field_info.alias if field_info.alias else updated_field + data[field_name] = updated_value # create the update config commands with the modified data command_context = self.storage_service.variant_study_service.command_factory.command_context @@ -381,7 +381,7 @@ 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} + values = {**new_properties.model_dump(mode="json", by_alias=False), "id": cluster_id} return ThermalClusterOutput.model_validate(values) def delete_clusters(self, study: Study, area_id: str, cluster_ids: t.Sequence[str]) -> None: @@ -431,9 +431,8 @@ def duplicate_cluster( Raises: DuplicateThermalCluster: If a cluster with the new name already exists in the area. """ - new_id = transform_name_to_id(new_cluster_name, lower=False) - lower_new_id = new_id.lower() - if any(lower_new_id == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): + new_id = transform_name_to_id(new_cluster_name) + if any(new_id == cluster.id for cluster in self.get_clusters(study, area_id)): raise DuplicateThermalCluster(area_id, new_id) # Cluster duplication @@ -445,23 +444,22 @@ def duplicate_cluster( create_cluster_cmd = self._make_create_cluster_cmd(area_id, new_config, study_version) # Matrix edition - lower_source_id = source_id.lower() source_paths = [ - f"input/thermal/series/{area_id}/{lower_source_id}/series", - f"input/thermal/prepro/{area_id}/{lower_source_id}/modulation", - f"input/thermal/prepro/{area_id}/{lower_source_id}/data", + f"input/thermal/series/{area_id}/{source_id}/series", + f"input/thermal/prepro/{area_id}/{source_id}/modulation", + f"input/thermal/prepro/{area_id}/{source_id}/data", ] new_paths = [ - f"input/thermal/series/{area_id}/{lower_new_id}/series", - f"input/thermal/prepro/{area_id}/{lower_new_id}/modulation", - f"input/thermal/prepro/{area_id}/{lower_new_id}/data", + f"input/thermal/series/{area_id}/{new_id}/series", + f"input/thermal/prepro/{area_id}/{new_id}/modulation", + f"input/thermal/prepro/{area_id}/{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") - new_paths.append(f"input/thermal/series/{area_id}/{lower_new_id}/CO2Cost") - new_paths.append(f"input/thermal/series/{area_id}/{lower_new_id}/fuelCost") + source_paths.append(f"input/thermal/series/{area_id}/{source_id}/CO2Cost") + source_paths.append(f"input/thermal/series/{area_id}/{source_id}/fuelCost") + new_paths.append(f"input/thermal/series/{area_id}/{new_id}/CO2Cost") + new_paths.append(f"input/thermal/series/{area_id}/{new_id}/fuelCost") # Prepare and execute commands commands: t.List[t.Union[CreateCluster, ReplaceMatrix]] = [create_cluster_cmd] @@ -479,8 +477,7 @@ def duplicate_cluster( return ThermalClusterOutput(**new_config.model_dump(mode="json", by_alias=False)) def validate_series(self, study: Study, 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}") + thermal_cluster_path = Path(f"input/thermal/series/{area_id}/{cluster_id.lower()}") series_path = [thermal_cluster_path / "series"] if StudyVersion.parse(study.version) >= STUDY_VERSION_8_7: series_path.append(thermal_cluster_path / "CO2Cost") diff --git a/antarest/study/business/binding_constraint_management.py b/antarest/study/business/binding_constraint_management.py index 1fb5d48a0d..8a35e0bbe8 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.serialization 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.field_validators 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 ( @@ -340,7 +340,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 2479760866..00e61d95fa 100644 --- a/antarest/study/business/district_manager.py +++ b/antarest/study/business/district_manager.py @@ -16,7 +16,7 @@ from antarest.core.serialization 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.field_validators 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/business/table_mode_management.py b/antarest/study/business/table_mode_management.py index efaeeae5c7..931f5b6c3e 100644 --- a/antarest/study/business/table_mode_management.py +++ b/antarest/study/business/table_mode_management.py @@ -219,7 +219,9 @@ def update_table_data( thermals_by_areas = collections.defaultdict(dict) for key, values in data.items(): area_id, cluster_id = key.split(" / ") - thermals_by_areas[area_id][cluster_id] = ThermalClusterInput(**values) + # Thermal clusters ids were not lowered at the time. + # So to ensure this endpoint still works with old scripts we have to lower the id at first. + thermals_by_areas[area_id][cluster_id.lower()] = ThermalClusterInput(**values) thermals_map = self._thermal_manager.update_thermals_props(study, thermals_by_areas) data = { f"{area_id} / {cluster_id}": cluster.model_dump(by_alias=True, exclude={"id", "name"}) @@ -232,7 +234,8 @@ def update_table_data( renewables_by_areas = collections.defaultdict(dict) for key, values in data.items(): area_id, cluster_id = key.split(" / ") - renewables_by_areas[area_id][cluster_id] = RenewableClusterInput(**values) + # Same reason as for thermal clusters + renewables_by_areas[area_id][cluster_id.lower()] = RenewableClusterInput(**values) renewables_map = self._renewable_manager.update_renewables_props(study, renewables_by_areas) data = { f"{area_id} / {cluster_id}": cluster.model_dump(by_alias=True, exclude={"id", "name"}) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py index f26f2b45e4..f35eb13a3b 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py @@ -21,6 +21,7 @@ from pydantic import Field +from antarest.core.model import LowerCaseStr from antarest.core.serialization import AntaresBaseModel @@ -47,9 +48,9 @@ class ItemProperties( [('group-A', 'cluster-01'), ('GROUP-A', 'cluster-02'), ('Group-B', 'CLUSTER-01')] """ - group: str = Field(default="", description="Cluster group") + group: LowerCaseStr = Field(default="", description="Cluster group") - name: str = Field(description="Cluster name", pattern=r"[a-zA-Z0-9_(),& -]+") + name: LowerCaseStr = Field(description="Cluster name", pattern=r"[a-zA-Z0-9_(),& -]+") def __lt__(self, other: t.Any) -> bool: """ @@ -58,7 +59,7 @@ def __lt__(self, other: t.Any) -> bool: 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 (self.group, self.name).__lt__((other.group, other.name)) return NotImplemented 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 5089150c71..8c111222bb 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import re import typing as t _ALL_FILTERING = ["hourly", "daily", "weekly", "monthly", "annual"] @@ -87,3 +87,28 @@ def validate_color_rgb(v: t.Any) -> str: raise TypeError(f"Invalid type for 'color_rgb': {type(v)}") return f"#{r:02X}{g:02X}{b:02X}" + + +# 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) -> str: + """ + Transform a name into an identifier by replacing consecutive + invalid characters by a single white space, then whitespaces + are striped from both ends and the id is lowered. + + Valid characters are `[a-zA-Z0-9_(),& -]` (including space). + + Args: + name: The name to convert. + """ + return _sub_invalid_chars(" ", name).strip().lower() + + +def validate_id_against_name(name: str) -> str: + to_return = transform_name_to_id(name) + if not to_return: + raise ValueError("Cluster name must only contains [a-zA-Z0-9],&,-,_,(,) characters") + return to_return diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/files.py b/antarest/study/storage/rawstudy/model/filesystem/config/files.py index 52458c9970..2931e7ac00 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/files.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/files.py @@ -42,7 +42,10 @@ SimulationParsingError, XpansionParsingError, ) -from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import extract_filtering +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import ( + extract_filtering, + transform_name_to_id, +) from antarest.study.storage.rawstudy.model.filesystem.config.model import ( Area, BindingConstraintDTO, @@ -50,7 +53,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 e25c5ab9fb..aaf49cb7ef 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py @@ -14,14 +14,12 @@ from pydantic import Field, model_validator -__all__ = ("IgnoreCaseIdentifier", "LowerCaseIdentifier") - -from typing_extensions import override +__all__ = "LowerCaseIdentifier" from antarest.core.serialization import AntaresBaseModel -class IgnoreCaseIdentifier( +class LowerCaseIdentifier( AntaresBaseModel, extra="forbid", validate_assignment=True, @@ -45,9 +43,9 @@ 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 + from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id - return transform_name_to_id(name, lower=False) + return transform_name_to_id(name) @model_validator(mode="before") def validate_id(cls, values: t.MutableMapping[str, t.Any]) -> t.Mapping[str, t.Any]: @@ -76,28 +74,3 @@ def validate_id(cls, values: t.MutableMapping[str, t.Any]) -> t.Mapping[str, t.A else: raise ValueError(f"Invalid name '{name}'.") return values - - -class LowerCaseIdentifier(IgnoreCaseIdentifier): - """ - Base class for all configuration sections with a lower case ID. - """ - - id: str = Field(description="ID (section name)", pattern=r"[a-z0-9_(),& -]+") - - @classmethod - @override - def generate_id(cls, name: str) -> str: - """ - Generate an ID from a name. - - Args: - name: Name of a section read from an INI file - - Returns: - 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) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/model.py b/antarest/study/storage/rawstudy/model/filesystem/config/model.py index 388bfaeb5d..379787c007 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/model.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/model.py @@ -287,26 +287,6 @@ def get_filters_year(self, area: str, link: t.Optional[str] = None) -> t.List[st 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 6b8087b3fd..a72d2e48dc 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/renewable.py @@ -19,7 +19,7 @@ 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 LowerCaseIdentifier class TimeSeriesInterpretation(EnumIgnoreCase): @@ -45,15 +45,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 +68,7 @@ def _missing_(cls, value: object) -> t.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 t.cast(RenewableClusterGroup, super()._missing_(value)) # If a group is not found, return the default group ('OTHER1' by default). return cls.OTHER1 @@ -94,7 +94,7 @@ class RenewableProperties(ClusterProperties): ) -class RenewableConfig(RenewableProperties, IgnoreCaseIdentifier): +class RenewableConfig(RenewableProperties, LowerCaseIdentifier): """ Configuration of a renewable cluster. @@ -113,6 +113,7 @@ class RenewableConfig(RenewableProperties, IgnoreCaseIdentifier): RenewableConfigType = RenewableConfig +RenewablePropertiesType = RenewableProperties def get_renewable_config_cls(study_version: StudyVersion) -> t.Type[RenewableConfig]: @@ -130,6 +131,25 @@ def get_renewable_config_cls(study_version: StudyVersion) -> t.Type[RenewableCon raise ValueError(f"Unsupported study version {study_version}, required 810 or above.") +def create_renewable_properties(study_version: StudyVersion, **kwargs: t.Any) -> RenewablePropertiesType: + """ + Factory method to create renewable properties. + + Args: + study_version: The version of the study. + **kwargs: 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(kwargs) + raise ValueError(f"Unsupported study version {study_version}, required 810 or above.") + + def create_renewable_config(study_version: StudyVersion, **kwargs: t.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 0d21455783..fc3e2d226e 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/st_storage.py @@ -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 @@ -161,6 +161,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 = t.Union[STStorageConfig, STStorage880Config] +STStoragePropertiesType = t.Union[STStorageProperties, STStorage880Properties] + + +def create_st_storage_properties(study_version: StudyVersion, **kwargs: t.Any) -> STStoragePropertiesType: + """ + Factory method to create st_storage properties. + + Args: + study_version: The version of the study. + **kwargs: The properties 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(kwargs) + elif study_version >= STUDY_VERSION_8_6: + return STStorageProperties.model_validate(kwargs) + raise ValueError(f"Unsupported study version: {study_version}") def get_st_storage_config_cls(study_version: StudyVersion) -> t.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 87f20514f4..4a75756728 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/thermal.py @@ -13,12 +13,12 @@ import typing as t from antares.study.version import StudyVersion -from pydantic import Field +from pydantic import Field, ValidationError from typing_extensions import override 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 LowerCaseIdentifier class LocalTSGenerationBehavior(EnumIgnoreCase): @@ -61,16 +61,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 +85,8 @@ def _missing_(cls, value: object) -> t.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 t.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 t.cast(t.Optional["ThermalClusterGroup"], super()._missing_(value)) @@ -335,7 +333,7 @@ class Thermal870Properties(Thermal860Properties): ) -class ThermalConfig(ThermalProperties, IgnoreCaseIdentifier): +class ThermalConfig(ThermalProperties, LowerCaseIdentifier): """ Thermal properties with section ID. @@ -356,7 +354,7 @@ class ThermalConfig(ThermalProperties, IgnoreCaseIdentifier): AttributeError: 'ThermalConfig' object has no attribute 'nh3'""" -class Thermal860Config(Thermal860Properties, IgnoreCaseIdentifier): +class Thermal860Config(Thermal860Properties, LowerCaseIdentifier): """ Thermal properties for study in version 860 @@ -378,7 +376,7 @@ class Thermal860Config(Thermal860Properties, IgnoreCaseIdentifier): """ -class Thermal870Config(Thermal870Properties, IgnoreCaseIdentifier): +class Thermal870Config(Thermal870Properties, LowerCaseIdentifier): """ Thermal properties for study in version 8.7 or above. @@ -409,6 +407,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 = t.Union[Thermal870Config, Thermal860Config, ThermalConfig] +ThermalPropertiesType = t.Union[Thermal870Properties, Thermal860Properties, ThermalProperties] def get_thermal_config_cls(study_version: StudyVersion) -> t.Type[ThermalConfigType]: @@ -429,6 +428,28 @@ def get_thermal_config_cls(study_version: StudyVersion) -> t.Type[ThermalConfigT return ThermalConfig +def create_thermal_properties(study_version: StudyVersion, **kwargs: t.Any) -> ThermalPropertiesType: + """ + Factory method to create thermal properties. + + Args: + study_version: The version of the study. + **kwargs: 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(kwargs) + elif study_version == 860: + return Thermal860Properties.model_validate(kwargs) + else: + return ThermalProperties.model_validate(kwargs) + + def create_thermal_config(study_version: StudyVersion, **kwargs: t.Any) -> ThermalConfigType: """ Factory method to create a thermal configuration model. diff --git a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py index b3c3443dee..d757c20e96 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py +++ b/antarest/study/storage/rawstudy/model/filesystem/root/input/thermal/prepro/area/area.py @@ -32,11 +32,8 @@ def __init__( @override def build(self) -> TREE: - # Note that cluster IDs are case-insensitive, but series IDs are in lower case. - # For instance, if your cluster ID is "Base", then the series ID will be "base". - series_ids = map(str.lower, self.config.get_thermal_ids(self.area)) children: TREE = { series_id: InputThermalPreproAreaThermal(self.context, self.config.next_file(series_id)) - for series_id in series_ids + for series_id in self.config.get_thermal_ids(self.area) } return children diff --git a/antarest/study/storage/variantstudy/business/command_extractor.py b/antarest/study/storage/variantstudy/business/command_extractor.py index 95b51fe9ea..94319e7f0e 100644 --- a/antarest/study/storage/variantstudy/business/command_extractor.py +++ b/antarest/study/storage/variantstudy/business/command_extractor.py @@ -225,7 +225,7 @@ def _extract_cluster(self, study: FileStudy, area_id: str, cluster_id: str, rene create_cluster_command( area_id=area_id, cluster_name=cluster.id, - parameters=cluster.model_dump(by_alias=True, exclude_defaults=True, exclude={"id"}), + parameters=cluster.model_dump(mode="json", by_alias=True, exclude={"id"}), command_context=self.command_context, study_version=study_tree.config.version, ), diff --git a/antarest/study/storage/variantstudy/business/command_reverter.py b/antarest/study/storage/variantstudy/business/command_reverter.py index d5971dd69e..7ed581aa57 100644 --- a/antarest/study/storage/variantstudy/business/command_reverter.py +++ b/antarest/study/storage/variantstudy/business/command_reverter.py @@ -15,7 +15,7 @@ from pathlib import Path from antarest.core.exceptions import ChildNotFoundError -from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id 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 @@ -222,7 +222,7 @@ def _revert_create_st_storage( history: t.List["ICommand"], base: FileStudy, ) -> t.List[ICommand]: - storage_id = base_command.parameters.id + storage_id = base_command.storage_id return [ RemoveSTStorage( area_id=base_command.area_id, diff --git a/antarest/study/storage/variantstudy/model/command/create_area.py b/antarest/study/storage/variantstudy/model/command/create_area.py index 65c78399b9..1f9c6dd187 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.field_validators 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 MATCH_SIGNATURE_SEPARATOR, ICommand 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 b76f092086..407e429283 100644 --- a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py @@ -19,6 +19,7 @@ from pydantic import Field, field_validator, model_validator from typing_extensions import override +from antarest.core.model import LowerCaseStr from antarest.core.serialization import AntaresBaseModel from antarest.matrixstore.model import MatrixData from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model @@ -30,8 +31,11 @@ BindingConstraintFrequency, 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.field_validators import ( + transform_name_to_id, + validate_filtering, +) +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 @@ -118,7 +122,7 @@ def _validate_filtering(cls, v: t.Any) -> str: class BindingConstraintProperties870(BindingConstraintProperties830): - group: str = DEFAULT_GROUP + group: LowerCaseStr = DEFAULT_GROUP BindingConstraintProperties = t.Union[ @@ -342,8 +346,8 @@ def apply_binding_constraint( elif "." in link_or_cluster: # Cluster IDs are stored in lower case in the binding constraints file. area, cluster_id = link_or_cluster.split(".") - thermal_ids = {thermal.id.lower() for thermal in study_data.config.areas[area].thermals} - if area not in study_data.config.areas or cluster_id.lower() not in thermal_ids: + thermal_ids = {thermal.id for thermal in study_data.config.areas[area].thermals} + if area not in study_data.config.areas or cluster_id not in thermal_ids: return CommandOutput( status=False, message=f"Cluster '{link_or_cluster}' does not exist in binding constraint '{bd_id}'", diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py index c8f42fb60a..848c200466 100644 --- a/antarest/study/storage/variantstudy/model/command/create_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py @@ -12,19 +12,20 @@ import typing as t -from pydantic import Field, ValidationInfo, field_validator +from pydantic import Field, model_validator from typing_extensions import override -from antarest.core.model import JSON +from antarest.core.model import JSON, LowerCaseStr 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 transform_name_to_id +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 @@ -48,44 +49,30 @@ class CreateCluster(ICommand): # ================== area_id: str - cluster_name: str - parameters: t.Dict[str, t.Any] + cluster_name: LowerCaseStr + parameters: ThermalPropertiesType prepro: t.Optional[t.Union[t.List[t.List[MatrixData]], str]] = Field(None, validate_default=True) modulation: t.Optional[t.Union[t.List[t.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: t.Optional[t.Union[t.List[t.List[MatrixData]], str]], - values: t.Union[t.Dict[str, t.Any], ValidationInfo], - ) -> t.Optional[t.Union[t.List[t.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 + @model_validator(mode="before") + def validate_model(cls, values: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + # Validate parameters + args = {"name": values["cluster_name"], **values["parameters"]} + values["parameters"] = create_thermal_properties(values["study_version"], **args) + + # 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: t.Optional[t.Union[t.List[t.List[MatrixData]], str]], - values: t.Union[t.Dict[str, t.Any], ValidationInfo], - ) -> t.Optional[t.Union[t.List[t.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) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: @@ -128,12 +115,11 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = 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 +140,7 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = } } } - 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) @@ -168,7 +154,7 @@ def to_dto(self) -> CommandDTO: 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), }, @@ -192,9 +178,11 @@ def match(self, other: ICommand, equal: bool = False) -> bool: simple_match = self.area_id == other.area_id and self.cluster_name == other.cluster_name if not equal: return simple_match + self_params = self.parameters.model_dump(mode="json", by_alias=True) + other_params = other.parameters.model_dump(mode="json", by_alias=True) return ( simple_match - and self.parameters == other.parameters + and self_params == other_params and self.prepro == other.prepro and self.modulation == other.modulation ) @@ -206,7 +194,7 @@ def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig # Series identifiers are in lower case. - series_id = transform_name_to_id(self.cluster_name, lower=True) + series_id = transform_name_to_id(self.cluster_name) commands: t.List[ICommand] = [] if self.prepro != other.prepro: commands.append( @@ -226,11 +214,13 @@ def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: study_version=self.study_version, ) ) - if self.parameters != other.parameters: + self_params = self.parameters.model_dump(mode="json", by_alias=True) + other_params = other.parameters.model_dump(mode="json", by_alias=True) + if self_params != other_params: commands.append( UpdateConfig( target=f"input/thermal/clusters/{self.area_id}/list/{self.cluster_name}", - data=other.parameters, + data=other_params, command_context=self.command_context, study_version=self.study_version, ) diff --git a/antarest/study/storage/variantstudy/model/command/create_district.py b/antarest/study/storage/variantstudy/model/command/create_district.py index 8edb484700..63a6e5c3d6 100644 --- a/antarest/study/storage/variantstudy/model/command/create_district.py +++ b/antarest/study/storage/variantstudy/model/command/create_district.py @@ -16,11 +16,9 @@ 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.core.model import LowerCaseStr +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators 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 MATCH_SIGNATURE_SEPARATOR, ICommand @@ -53,13 +51,6 @@ class CreateDistrict(ICommand): output: bool = True comments: str = "" - @field_validator("name") - def validate_district_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 - @override def _apply_config(self, study_data: FileStudyTreeConfig) -> Tuple[CommandOutput, Dict[str, Any]]: district_id = transform_name_to_id(self.name) 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 2b4d93a085..342bf32690 100644 --- a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py @@ -12,17 +12,16 @@ import typing as t -from pydantic import field_validator +from pydantic import 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.core.model import JSON, LowerCaseStr +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 MATCH_SIGNATURE_SEPARATOR, ICommand @@ -45,15 +44,15 @@ class CreateRenewablesCluster(ICommand): # ================== area_id: str - cluster_name: str - parameters: t.Dict[str, t.Any] + cluster_name: LowerCaseStr + parameters: RenewableProperties - @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") + def validate_model(cls, values: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + # Validate parameters + args = {"name": values["cluster_name"], **values["parameters"]} + values["parameters"] = create_renewable_properties(values["study_version"], **args) + return values @override def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: @@ -109,14 +108,9 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = 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() @@ -143,7 +137,7 @@ def to_dto(self) -> CommandDTO: 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, ) @@ -165,7 +159,9 @@ def match(self, other: ICommand, equal: bool = False) -> bool: simple_match = self.area_id == other.area_id and self.cluster_name == other.cluster_name if not equal: return simple_match - return simple_match and self.parameters == other.parameters + self_params = self.parameters.model_dump(mode="json", by_alias=True) + other_params = other.parameters.model_dump(mode="json", by_alias=True) + return simple_match and self_params == other_params @override def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: @@ -173,11 +169,13 @@ def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig commands: t.List[ICommand] = [] - if self.parameters != other.parameters: + self_params = self.parameters.model_dump(mode="json", by_alias=True) + other_params = other.parameters.model_dump(mode="json", by_alias=True) + if self_params != other_params: commands.append( UpdateConfig( target=f"input/renewables/clusters/{self.area_id}/list/{self.cluster_name}", - data=other.parameters, + data=other_params, command_context=self.command_context, 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 a3c16195ad..0a1bfa3589 100644 --- a/antarest/study/storage/variantstudy/model/command/create_st_storage.py +++ b/antarest/study/storage/variantstudy/model/command/create_st_storage.py @@ -16,11 +16,16 @@ from pydantic import Field, ValidationInfo, model_validator from typing_extensions import override -from antarest.core.model import JSON +from antarest.core.model import JSON, LowerCaseStr 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 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 @@ -59,8 +64,8 @@ class CreateSTStorage(ICommand): # Command parameters # ================== - area_id: str = Field(description="Area ID", pattern=r"[a-z0-9_(),& -]+") - parameters: STStorageConfigType + area_id: LowerCaseStr = Field(description="Area ID", pattern=r"[a-z0-9_(),& -]+") + parameters: STStoragePropertiesType pmax_injection: t.Optional[t.Union[MatrixType, str]] = Field( default=None, description="Charge capacity (modulation)", @@ -85,13 +90,18 @@ 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") + def validate_model(cls, values: t.Dict[str, t.Any]) -> t.Dict[str, t.Any]: + values["parameters"] = create_st_storage_properties(values["study_version"], **values["parameters"]) + return values + @staticmethod def validate_field( v: t.Optional[t.Union[MatrixType, str]], values: t.Dict[str, t.Any], field: str @@ -175,6 +185,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutpu """ # 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 ( @@ -197,7 +208,7 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutpu 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, @@ -207,14 +218,17 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutpu ) # 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 @@ -230,6 +244,7 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = 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 @@ -237,13 +252,13 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = # 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}}}, } } } @@ -260,12 +275,11 @@ 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, 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, @@ -319,9 +333,10 @@ def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig other = t.cast(CreateSTStorage, other) + storage_id = self.storage_id commands: t.List[ICommand] = [ ReplaceMatrix( - target=f"input/st-storage/series/{self.area_id}/{self.storage_id}/{attr}", + target=f"input/st-storage/series/{self.area_id}/{storage_id}/{attr}", matrix=strip_matrix_protocol(getattr(other, attr)), command_context=self.command_context, study_version=self.study_version, @@ -329,12 +344,13 @@ def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: for attr in _MATRIX_NAMES if getattr(self, attr) != getattr(other, attr) ] - if self.parameters != other.parameters: - data: t.Dict[str, t.Any] = other.parameters.model_dump(mode="json", by_alias=True, exclude={"id"}) + self_params = self.parameters.model_dump(mode="json", by_alias=True) + other_params = other.parameters.model_dump(mode="json", by_alias=True) + if self_params != other_params: commands.append( UpdateConfig( - target=f"input/st-storage/clusters/{self.area_id}/list/{self.storage_id}", - data=data, + target=f"input/st-storage/clusters/{self.area_id}/list/{storage_id}", + data=other_params, command_context=self.command_context, study_version=self.study_version, ) diff --git a/antarest/study/storage/variantstudy/model/command/remove_cluster.py b/antarest/study/storage/variantstudy/model/command/remove_cluster.py index 3fe682ead9..01edecf76e 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/remove_cluster.py @@ -12,8 +12,10 @@ import typing as t +from pydantic import Field from typing_extensions import override +from antarest.core.model import LowerCaseStr from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.utils_binding_constraint import ( @@ -40,7 +42,7 @@ class RemoveCluster(ICommand): # ================== area_id: str - cluster_id: str + cluster_id: LowerCaseStr = Field(description="Cluster ID", pattern=r"[a-z0-9_(),& -]+") @override def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: diff --git a/antarest/study/storage/variantstudy/model/command/remove_link.py b/antarest/study/storage/variantstudy/model/command/remove_link.py index 2f9b2fa7ef..ab30b9eb15 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.field_validators 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 MATCH_SIGNATURE_SEPARATOR, ICommand, OutputTuple @@ -47,7 +48,7 @@ class RemoveLink(ICommand): def _validate_id(cls, area: str) -> str: if isinstance(area, str): # Area IDs must be in lowercase and not empty. - area_id = transform_name_to_id(area, lower=True) + area_id = transform_name_to_id(area) if area_id: return area_id # Valid characters are `[a-zA-Z0-9_(),& -]` (including space). 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 5947889bbe..0e593fdf2c 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/remove_renewables_cluster.py @@ -12,8 +12,10 @@ import typing as t +from pydantic import Field from typing_extensions import override +from antarest.core.model import LowerCaseStr from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput @@ -37,7 +39,7 @@ class RemoveRenewablesCluster(ICommand): # ================== area_id: str - cluster_id: str + cluster_id: LowerCaseStr = Field(description="Cluster ID", pattern=r"[a-z0-9_(),& -]+") @override def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: 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 a5a420a362..910eebd08b 100644 --- a/antarest/study/storage/variantstudy/model/command/remove_st_storage.py +++ b/antarest/study/storage/variantstudy/model/command/remove_st_storage.py @@ -15,6 +15,7 @@ from pydantic import Field from typing_extensions import override +from antarest.core.model import LowerCaseStr from antarest.study.model import STUDY_VERSION_8_6 from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy @@ -42,7 +43,7 @@ class RemoveSTStorage(ICommand): # ================== area_id: str = Field(description="Area ID", pattern=r"[a-z0-9_(),& -]+") - storage_id: str = Field(description="Short term storage ID", pattern=r"[a-z0-9_(),& -]+") + storage_id: LowerCaseStr = Field(description="Short term storage ID", pattern=r"[a-z0-9_(),& -]+") @override def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 9be21c1743..b535a0526f 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -21,7 +21,7 @@ from antarest.core.config import Config from antarest.core.jwt import JWTUser -from antarest.core.model import JSON, StudyPermissionType +from antarest.core.model import JSON, LowerCaseStr, StudyPermissionType from antarest.core.requests import RequestParameters from antarest.core.utils.utils import sanitize_uuid from antarest.core.utils.web import APITag @@ -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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.config.ruleset_matrices import TableForm as SBTableForm logger = logging.getLogger(__name__) @@ -1976,7 +1976,7 @@ def create_renewable_cluster( def update_renewable_cluster( uuid: str, area_id: str, - cluster_id: str, + cluster_id: LowerCaseStr, cluster_data: RenewableClusterInput, current_user: JWTUser = Depends(auth.get_current_user), ) -> RenewableClusterOutput: @@ -2150,7 +2150,7 @@ def create_thermal_cluster( def update_thermal_cluster( uuid: str, area_id: str, - cluster_id: str, + cluster_id: LowerCaseStr, cluster_data: ThermalClusterInput, current_user: JWTUser = Depends(auth.get_current_user), ) -> ThermalClusterOutput: @@ -2577,8 +2577,8 @@ def duplicate_cluster( uuid: str, area_id: str, cluster_type: ClusterType, - source_cluster_id: str, - new_cluster_name: str = Query(..., alias="newName", title="New Cluster Name"), + source_cluster_id: LowerCaseStr, + new_cluster_name: LowerCaseStr = Query(..., alias="newName", title="New Cluster Name"), current_user: JWTUser = Depends(auth.get_current_user), ) -> t.Union[STStorageOutput, ThermalClusterOutput, RenewableClusterOutput]: logger.info( diff --git a/tests/integration/assets/base_study.zip b/tests/integration/assets/base_study.zip index 3b282beb1693044a666fc204fee083eb453c18ba..95665329911994489ba14d3331315d03ca2909b9 100644 GIT binary patch delta 75253 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 Y`F;IRgrkn54)(E+&y}`t0*BB41AqhW$N&HU delta 61139 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 X^Tt&LC^(Kgk2`a}aX97yhQslH2yMnz diff --git a/tests/integration/assets/variant_study.zip b/tests/integration/assets/variant_study.zip index 4526fc900f0abdfa80347895cd8880f9fe986eb9..9cfb20f19e0c1b7a02be8955bbd64c1375535e64 100644 GIT binary patch literal 309622 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 literal 311282 zcmeEv1zeU%_cq-rodQaChk$fRhjcg6T?*2TfS`0EAV`S_A|)Y>w5X&sN{NE`-S>mR zt}Bba`|j)i`|<4FqV7I3XU<%6otQIoE1!df!-WDr&s2ESAiw$i3kDPgl)JIBwXwac zp^K}VnHQ^uIyw|0%z~V8lMs#hec*BPM1_KeI~USw4f#d+b6ZG|wqyt+!pwlSoPmev zyS60ET`ssfII>vVTYqgfq42`QTmPJi021wS`oK7ZWO4G8GM5sWFzQ&g1l^JptHq)7qqt;yubim>RjVqI=WX zkK%f~uYatn-if~a;fiJPMGbt`FQR|12HfvNx3+h5bN!}_=EIc%1vU34#S;IZMPp}k zW0(DQQ2{0YZM%P-u#<5_Fv}}?Cb~g8XYZ)(!z!%`G#K-(>&DQbUn%`z=0+h>AHE`W zyT}p~$BOz3I{GqYAA8m_SE+(GqS*@WsXZ)gRdWy?Dd<}D$hpm75E0^SLpp$uI&0dk zecp_*NsnU9LQ?t6u*XN%RDv&%*w=-toX(yupo?hWq5gYaxLF@X1CcDfQS-&hI)`%-omqQ&LjXh!DD+6Eha^6Ahlh@qhUW5u4-D3 zi@r0=pT$fb_J�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℞6=&5 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 860: for key in expected: @@ -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, @@ -864,7 +864,7 @@ def test_lifecycle__nominal( ) assert res.status_code == 200, res.json() cluster_id = res.json()["id"] - assert cluster_id == "Cluster 1" + assert cluster_id == "cluster 1" # Create Binding Constraints res = client.post( @@ -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..0ad7c7e881 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.field_validators 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", @@ -374,12 +374,14 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st ) assert res.status_code == 200, res.json() fr_gas_conventional_id = res.json()["id"] - assert fr_gas_conventional_id == transform_name_to_id(fr_gas_conventional, lower=False) + assert fr_gas_conventional_id == transform_name_to_id(fr_gas_conventional) # noinspection SpellCheckingInspection fr_gas_conventional_cfg = { **fr_gas_conventional_props, "id": fr_gas_conventional_id, **{p: pollutants_values for p in pollutants_names}, + "name": fr_gas_conventional_props["name"].lower(), + "group": fr_gas_conventional_props["group"].lower(), } fr_gas_conventional_cfg = { **fr_gas_conventional_cfg, @@ -426,17 +428,18 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.json() == EXISTING_CLUSTERS + [fr_gas_conventional_cfg] # updating properties + name = "FR_Gas conventional old 1" res = client.patch( f"/v1/studies/{internal_study_id}/areas/{area_id}/clusters/thermal/{fr_gas_conventional_id}", json={ - "name": "FR_Gas conventional old 1", + "name": name, "nominalCapacity": 32.1, }, ) assert res.status_code == 200, res.json() fr_gas_conventional_cfg = { **fr_gas_conventional_cfg, - "name": "FR_Gas conventional old 1", + "name": name.lower(), "nominalCapacity": 32.1, } assert res.json() == fr_gas_conventional_cfg @@ -509,8 +512,8 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.status_code in {200, 201}, res.json() # asserts the config is the same duplicated_config = dict(fr_gas_conventional_cfg) - duplicated_config["name"] = new_name - duplicated_id = transform_name_to_id(new_name, lower=False) + duplicated_config["name"] = new_name.lower() + duplicated_id = transform_name_to_id(new_name) duplicated_config["id"] = duplicated_id # takes the update into account if version >= 860: @@ -626,7 +629,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st ) assert res.status_code == 403, res.json() description = res.json()["description"] - assert all([elm in description for elm in [fr_gas_conventional, "binding constraint"]]) + assert all([elm in description for elm in [fr_gas_conventional.lower(), "binding constraint"]]) assert res.json()["exception"] == "ReferencedObjectDeletionNotAllowed" # delete the binding constraint @@ -667,7 +670,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.status_code == 200, res.json() deleted_clusters = [other_cluster_id1, other_cluster_id2, fr_gas_conventional_id] for cluster in res.json(): - assert transform_name_to_id(cluster["name"], lower=False) not in deleted_clusters + assert transform_name_to_id(cluster["name"]) not in deleted_clusters # =========================== # THERMAL CLUSTER ERRORS @@ -756,7 +759,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( @@ -836,7 +839,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.status_code == 409, res.json() obj = res.json() description = obj["description"] - assert new_name.upper() in description + assert new_name.lower() in description assert obj["exception"] == "DuplicateThermalCluster" @pytest.fixture(name="base_study_id") @@ -918,14 +921,14 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var ) assert res.status_code in {200, 201}, res.json() cluster_cfg = res.json() - assert cluster_cfg["name"] == new_name + assert cluster_cfg["name"] == new_name.lower() new_id = cluster_cfg["id"] # Check that the duplicate has the right properties 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 206227f9e9..91012cccd2 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", @@ -1338,7 +1338,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", # Default group used when not specified. "id": "cluster renewable 1", "name": "cluster renewable 1 renamed", "nominalCapacity": 3.0, @@ -1400,6 +1400,7 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None: f"/v1/studies/{study_id}/areas/area 1/clusters/thermal/cluster 1/form", ) assert res.status_code == 200, res.json() + obj["group"] = obj["group"].lower() assert res.json() == {"id": "cluster 1", **obj} # Links @@ -1472,19 +1473,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..d5c8154875 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.field_validators import transform_name_to_id from tests.integration.utils import wait_task_completion @@ -76,7 +76,7 @@ def test_lifecycle( area_fr_id = transform_name_to_id("FR") cluster_fr1 = "Oleron" - cluster_fr1_id = transform_name_to_id(cluster_fr1, lower=False) + cluster_fr1_id = transform_name_to_id(cluster_fr1) args = { "area_id": area_fr_id, "cluster_name": cluster_fr1_id, @@ -95,7 +95,7 @@ def test_lifecycle( res.raise_for_status() cluster_fr2 = "La_Rochelle" - cluster_fr2_id = transform_name_to_id(cluster_fr2, lower=False) + cluster_fr2_id = transform_name_to_id(cluster_fr2) args = { "area_id": area_fr_id, "cluster_name": cluster_fr2_id, @@ -124,9 +124,9 @@ def test_lifecycle( properties = res.json() expected = { "enabled": True, - "group": "Wind Offshore", - "id": "Oleron", - "name": cluster_fr1, + "group": "wind offshore", + "id": "oleron", + "name": cluster_fr1.lower(), "nominalCapacity": 2500.0, "tsInterpretation": "power-generation", "unitCount": 1, @@ -141,9 +141,9 @@ def test_lifecycle( properties = res.json() expected = { "enabled": False, - "group": "Solar PV", - "id": "La_Rochelle", - "name": cluster_fr2, + "group": "solar pv", + "id": "la_rochelle", + "name": cluster_fr2.lower(), "nominalCapacity": 3500.0, "tsInterpretation": "power-generation", "unitCount": 4, @@ -201,10 +201,10 @@ def test_lifecycle( area_it_id = transform_name_to_id("IT") cluster_it1 = "Oléron" - cluster_it1_id = transform_name_to_id(cluster_it1, lower=False) + cluster_it1_id = transform_name_to_id(cluster_it1) args = { "area_id": area_it_id, - "cluster_name": cluster_it1_id, + "cluster_name": cluster_it1, "parameters": { "group": "wind offshore", "name": cluster_it1, @@ -228,9 +228,9 @@ def test_lifecycle( properties = res.json() expected = { "enabled": True, - "group": "Wind Offshore", - "id": "Ol ron", - "name": cluster_it1, + "group": "wind offshore", + "id": "ol ron", + "name": cluster_it1.lower(), "nominalCapacity": 1000.0, "tsInterpretation": "production-factor", "unitCount": 1, @@ -274,9 +274,11 @@ def test_lifecycle( "list": { cluster_fr1_id: { "group": "wind offshore", - "name": cluster_fr1, + "name": cluster_fr1.lower(), "nominalcapacity": 2500, "ts-interpretation": "power-generation", + "unitcount": 1, + "enabled": True, }, } }, @@ -284,10 +286,11 @@ def test_lifecycle( "list": { cluster_it1_id: { "group": "wind offshore", - "name": cluster_it1, + "name": cluster_it1.lower(), "nominalcapacity": 1000, "ts-interpretation": "production-factor", "unitcount": 1, + "enabled": True, } } }, @@ -317,10 +320,11 @@ def test_lifecycle( "list": { cluster_it1_id: { "group": "wind offshore", - "name": cluster_it1, + "name": cluster_it1.lower(), "nominalcapacity": 1000, "ts-interpretation": "production-factor", "unitcount": 1, + "enabled": True, } } }, diff --git a/tests/integration/variant_blueprint/test_st_storage.py b/tests/integration/variant_blueprint/test_st_storage.py index 52ff483836..093dfc82b4 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.field_validators import transform_name_to_id from tests.integration.utils import wait_task_completion diff --git a/tests/integration/variant_blueprint/test_thermal_cluster.py b/tests/integration/variant_blueprint/test_thermal_cluster.py index 5029c2bca3..04ed66f669 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.field_validators import transform_name_to_id def _create_thermal_params(cluster_name: str) -> t.Mapping[str, t.Any]: @@ -97,7 +97,7 @@ def test_cascade_update( "action": "create_cluster", "args": { "area_id": area_id, - "cluster_name": transform_name_to_id(cluster_name, lower=False), + "cluster_name": transform_name_to_id(cluster_name), "parameters": _create_thermal_params(cluster_name), "prepro": np.random.rand(8760, 6).tolist(), "modulation": np.random.rand(8760, 4).tolist(), diff --git a/tests/storage/business/test_study_version_upgrader.py b/tests/storage/business/test_study_version_upgrader.py index c5fc6443dc..042459a401 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.study.storage.rawstudy.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.field_validators 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..3b748a377b 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.field_validators import transform_name_to_id VALID_CHARS = "azAZ09_-(),&" @@ -29,8 +29,7 @@ def test_transform_name_to_id__nominal_case(name, expected): @pytest.mark.parametrize("name", VALID_CHARS) def test_transform_name_to_id__valid_chars(name): - assert transform_name_to_id(name, lower=True) == name.lower() - assert transform_name_to_id(name, lower=False) == name + assert transform_name_to_id(name) == name.lower() @pytest.mark.parametrize("name", sorted(set(string.punctuation) - set(VALID_CHARS))) diff --git a/tests/study/business/areas/test_st_storage_management.py b/tests/study/business/areas/test_st_storage_management.py index 8ce3b3c910..4f22df97b2 100644 --- a/tests/study/business/areas/test_st_storage_management.py +++ b/tests/study/business/areas/test_st_storage_management.py @@ -158,7 +158,7 @@ def test_get_all_storages__nominal_case( "id": "storage1", "enabled": None, "group": STStorageGroup.BATTERY, - "name": "Storage1", + "name": "storage1", "injectionNominalCapacity": 1500.0, "withdrawalNominalCapacity": 1500.0, "reservoirCapacity": 20000.0, @@ -170,7 +170,7 @@ def test_get_all_storages__nominal_case( "id": "storage2", "enabled": None, "group": STStorageGroup.PSP_CLOSED, - "name": "Storage2", + "name": "storage2", "injectionNominalCapacity": 2000.0, "withdrawalNominalCapacity": 1500.0, "reservoirCapacity": 20000.0, @@ -182,7 +182,7 @@ def test_get_all_storages__nominal_case( "id": "storage3", "enabled": None, "group": STStorageGroup.PSP_CLOSED, - "name": "Storage3", + "name": "storage3", "injectionNominalCapacity": 1500.0, "withdrawalNominalCapacity": 1500.0, "reservoirCapacity": 21000.0, @@ -264,7 +264,7 @@ def test_get_st_storages__nominal_case( "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 1500.0, - "name": "Storage1", + "name": "storage1", "reservoirCapacity": 20000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, @@ -276,7 +276,7 @@ def test_get_st_storages__nominal_case( "initialLevel": 0.5, "initialLevelOptim": False, "injectionNominalCapacity": 2000.0, - "name": "Storage2", + "name": "storage2", "reservoirCapacity": 20000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, @@ -288,7 +288,7 @@ def test_get_st_storages__nominal_case( "initialLevel": 1, "initialLevelOptim": False, "injectionNominalCapacity": 1500.0, - "name": "Storage3", + "name": "storage3", "reservoirCapacity": 21000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, @@ -375,7 +375,7 @@ def test_get_st_storage__nominal_case( "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 1500.0, - "name": "Storage1", + "name": "storage1", "reservoirCapacity": 20000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, diff --git a/tests/study/business/areas/test_thermal_management.py b/tests/study/business/areas/test_thermal_management.py index 07bdb58787..64a351f7b2 100644 --- a/tests/study/business/areas/test_thermal_management.py +++ b/tests/study/business/areas/test_thermal_management.py @@ -373,7 +373,7 @@ def test_create_cluster__study_legacy( "fixedCost": 0.0, "genTs": LocalTSGenerationBehavior.USE_GLOBAL, "group": ThermalClusterGroup.NUCLEAR, - "id": "New Cluster", + "id": "new cluster", "lawForced": LawOption.UNIFORM, "lawPlanned": LawOption.UNIFORM, "marginalCost": 0.0, @@ -382,7 +382,7 @@ def test_create_cluster__study_legacy( "minStablePower": 0.0, "minUpTime": 15, "mustRun": False, - "name": "New Cluster", + "name": "new cluster", "nh3": None, "nmvoc": None, "nominalCapacity": 1000.0, @@ -430,7 +430,7 @@ def test_update_cluster( expected = { "id": "2 avail and must 1", "group": ThermalClusterGroup.GAS, - "name": "New name", + "name": "new name", "enabled": False, "unitCount": 100, "nominalCapacity": 2000.0, diff --git a/tests/study/storage/rawstudy/test_raw_study_service.py b/tests/study/storage/rawstudy/test_raw_study_service.py index c2f2b8a9c0..78264abfc4 100644 --- a/tests/study/storage/rawstudy/test_raw_study_service.py +++ b/tests/study/storage/rawstudy/test_raw_study_service.py @@ -25,7 +25,7 @@ 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 STStorageGroup 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,16 +131,15 @@ def test_export_study_flat( create_st_storage = CreateSTStorage( command_context=command_context, area_id="fr", - parameters=STStorageConfig( - id="", # will be calculated ;-) - name="Storage1", - group=STStorageGroup.BATTERY, - injection_nominal_capacity=1500, - withdrawal_nominal_capacity=1500, - reservoir_capacity=20000, - efficiency=0.94, - initial_level_optim=True, - ), + parameters={ + "name": "Storage1", + "group": STStorageGroup.BATTERY, + "injection_nominal_capacity": 1500, + "withdrawal_nominal_capacity": 1500, + "reservoir_capacity": 20000, + "efficiency": 0.94, + "initial_level_optim": True, + }, pmax_injection=pmax_injection.tolist(), inflows=inflows.tolist(), study_version=raw_study.version, diff --git a/tests/study/storage/variantstudy/test_snapshot_generator.py b/tests/study/storage/variantstudy/test_snapshot_generator.py index 6956502504..c93e50bfae 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 @@ -33,6 +32,7 @@ from antarest.core.utils.fastapi_sqlalchemy import db from antarest.login.model import Group, Role, User from antarest.study.model import RawStudy, Study, StudyAdditionalData +from antarest.study.storage.rawstudy.ini_reader import IniReader from antarest.study.storage.rawstudy.raw_study_service import RawStudyService from antarest.study.storage.variantstudy.model.dbmodel import CommandBlock, VariantStudy, VariantStudySnapshot from antarest.study.storage.variantstudy.model.model import CommandDTO @@ -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 54e1db01ab..f5eb4b5250 100644 --- a/tests/study/storage/variantstudy/test_variant_study_service.py +++ b/tests/study/storage/variantstudy/test_variant_study_service.py @@ -12,7 +12,6 @@ import datetime import re -import typing from pathlib import Path from unittest.mock import Mock, patch @@ -31,7 +30,7 @@ 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 STStorageGroup 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 @@ -201,16 +200,15 @@ def test_generate_task( create_st_storage = CreateSTStorage( command_context=command_context, area_id="fr", - parameters=STStorageConfig( - id="", # will be calculated ;-) - name="Storage1", - group=STStorageGroup.BATTERY, - injection_nominal_capacity=1500, - withdrawal_nominal_capacity=1500, - reservoir_capacity=20000, - efficiency=0.94, - initial_level_optim=True, - ), + parameters={ + "name": "Storage1", + "group": STStorageGroup.BATTERY, + "injection_nominal_capacity": 1500, + "withdrawal_nominal_capacity": 1500, + "reservoir_capacity": 20000, + "efficiency": 0.94, + "initial_level_optim": True, + }, pmax_injection=pmax_injection.tolist(), inflows=inflows.tolist(), study_version=study_version, diff --git a/tests/variantstudy/model/command/test_create_area.py b/tests/variantstudy/model/command/test_create_area.py index 8e6de24779..e9545c9885 100644 --- a/tests/variantstudy/model/command/test_create_area.py +++ b/tests/variantstudy/model/command/test_create_area.py @@ -18,7 +18,8 @@ from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.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.field_validators 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.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/model/command/test_create_cluster.py b/tests/variantstudy/model/command/test_create_cluster.py index bdd4e9feea..8f8a4d2b84 100644 --- a/tests/variantstudy/model/command/test_create_cluster.py +++ b/tests/variantstudy/model/command/test_create_cluster.py @@ -19,7 +19,8 @@ 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.field_validators import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.thermal import Thermal870Properties from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.common import CommandName @@ -57,13 +58,15 @@ def test_init(self, command_context: CommandContext): prepro_id = command_context.matrix_service.create(prepro) 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.cluster_name == "cluster1" + assert cl.parameters.group == "nuclear" + assert cl.parameters.nominal_capacity == 2400 + assert cl.parameters.unit_count == 2 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="%", @@ -95,16 +98,16 @@ def test_validate_modulation(self, command_context: CommandContext): def test_apply(self, empty_study: FileStudy, command_context: CommandContext): study_path = empty_study.config.study_path area_name = "DE" - area_id = transform_name_to_id(area_name, lower=True) + area_id = transform_name_to_id(area_name) cluster_name = "Cluster-1" - cluster_id = transform_name_to_id(cluster_name, lower=True) + cluster_id = transform_name_to_id(cluster_name) CreateArea(area_name=area_name, command_context=command_context, study_version=STUDY_VERSION_8_8).apply( empty_study ) parameters = { - "group": "Other", + "group": "nuclear", "unitcount": "1", "nominalcapacity": "1000000", "marginal-cost": "30", @@ -113,6 +116,7 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): prepro = GEN.random((365, 6)).tolist() modulation = GEN.random((8760, 4)).tolist() + command = CreateCluster( area_id=area_id, cluster_name=cluster_name, @@ -133,12 +137,13 @@ 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"]) + section = clusters[cluster_name.lower()] + assert str(section["name"]) == cluster_name.lower() + assert str(section["group"]) == parameters["group"] + assert int(section["unitcount"]) == int(parameters["unitcount"]) + assert float(section["nominalcapacity"]) == float(parameters["nominalcapacity"]) + assert float(section["marginal-cost"]) == float(parameters["marginal-cost"]) + assert float(section["market-bid-cost"]) == float(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() @@ -178,10 +183,11 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): def test_to_dto(self, command_context: CommandContext): prepro = GEN.random((365, 6)).tolist() modulation = GEN.random((8760, 4)).tolist() + parameters = {"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400} command = CreateCluster( area_id="foo", cluster_name="Cluster1", - parameters={"group": "Nuclear", "unitcount": 2, "nominalcapacity": 2400}, + parameters=parameters, command_context=command_context, prepro=prepro, modulation=modulation, @@ -194,8 +200,10 @@ def test_to_dto(self, command_context: CommandContext): "action": "create_cluster", "args": { "area_id": "foo", - "cluster_name": "Cluster1", - "parameters": {"group": "Nuclear", "nominalcapacity": 2400, "unitcount": 2}, + "cluster_name": "cluster1", + "parameters": Thermal870Properties.model_validate({"name": "cluster1", **parameters}).model_dump( + mode="json", by_alias=True + ), "prepro": prepro_id, "modulation": modulation_id, }, @@ -311,7 +319,9 @@ def test_create_diff(command_context: CommandContext): ), UpdateConfig( target="input/thermal/clusters/foo/list/foo", - data={"nominalcapacity": "2400"}, + data=Thermal870Properties.model_validate({"name": "foo", "nominalcapacity": "2400"}).model_dump( + mode="json", by_alias=True + ), command_context=command_context, study_version=STUDY_VERSION_8_8, ), diff --git a/tests/variantstudy/model/command/test_create_link.py b/tests/variantstudy/model/command/test_create_link.py index aa147e2df9..0c9a197735 100644 --- a/tests/variantstudy/model/command/test_create_link.py +++ b/tests/variantstudy/model/command/test_create_link.py @@ -21,7 +21,7 @@ from antarest.study.business.link_management import LinkInternal from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/model/command/test_create_renewables_cluster.py b/tests/variantstudy/model/command/test_create_renewables_cluster.py index 9919212a42..057da0f8e4 100644 --- a/tests/variantstudy/model/command/test_create_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_create_renewables_cluster.py @@ -18,7 +18,9 @@ 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.field_validators 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 from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.common import CommandName @@ -32,10 +34,11 @@ class TestCreateRenewablesCluster: # noinspection SpellCheckingInspection def test_init(self, command_context: CommandContext) -> None: + parameters = {"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400} cl = CreateRenewablesCluster( area_id="foo", cluster_name="Cluster1", - parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400}, + parameters=parameters, command_context=command_context, study_version=STUDY_VERSION_8_8, ) @@ -47,11 +50,13 @@ def test_init(self, command_context: CommandContext) -> None: # 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.cluster_name == "cluster1" + assert cl.parameters.model_dump(by_alias=True) == RenewableProperties.model_validate( + {"name": "cluster1", **parameters} + ).model_dump(by_alias=True) def test_validate_cluster_name(self, command_context: CommandContext) -> None: - with pytest.raises(ValidationError, match="cluster_name"): + with pytest.raises(ValidationError, match="name"): CreateRenewablesCluster( area_id="fr", cluster_name="%", @@ -66,7 +71,7 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> empty_study.config.version = study_version study_path = empty_study.config.study_path area_name = "DE" - area_id = transform_name_to_id(area_name, lower=True) + area_id = transform_name_to_id(area_name) cluster_name = "Cluster-1" CreateArea(area_name=area_name, command_context=command_context, study_version=study_version).apply(empty_study) @@ -94,8 +99,8 @@ 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.lower()]["name"]) == cluster_name.lower() + assert str(clusters[cluster_name.lower()]["ts-interpretation"]) == parameters["ts-interpretation"] output = CreateRenewablesCluster( area_id=area_id, @@ -137,10 +142,11 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> # noinspection SpellCheckingInspection def test_to_dto(self, command_context: CommandContext) -> None: + parameters = {"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400} command = CreateRenewablesCluster( area_id="foo", cluster_name="Cluster1", - parameters={"group": "Solar Thermal", "unitcount": 2, "nominalcapacity": 2400}, + parameters=parameters, command_context=command_context, study_version=STUDY_VERSION_8_8, ) @@ -149,8 +155,10 @@ 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}, + "cluster_name": "cluster1", + "parameters": RenewableProperties.model_validate({"name": "cluster1", **parameters}).model_dump( + by_alias=True + ), }, "id": None, "version": 1, @@ -223,17 +231,20 @@ def test_create_diff(command_context: CommandContext) -> None: command_context=command_context, study_version=STUDY_VERSION_8_8, ) + parameters = {"nominal_capacity": 1.2} other_match = CreateRenewablesCluster( area_id="foo", cluster_name="foo", - parameters={"a": "b"}, + parameters=parameters, command_context=command_context, study_version=STUDY_VERSION_8_8, ) assert base.create_diff(other_match) == [ UpdateConfig( target="input/renewables/clusters/foo/list/foo", - data={"a": "b"}, + data=RenewableProperties.model_validate({"name": "foo", **parameters}).model_dump( + mode="json", by_alias=True + ), command_context=command_context, study_version=STUDY_VERSION_8_8, ), diff --git a/tests/variantstudy/model/command/test_create_st_storage.py b/tests/variantstudy/model/command/test_create_st_storage.py index ba47d59da5..94f98fe4de 100644 --- a/tests/variantstudy/model/command/test_create_st_storage.py +++ b/tests/variantstudy/model/command/test_create_st_storage.py @@ -9,16 +9,19 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +import copy import re 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.st_storage import STStorageConfig +from antarest.study.model import STUDY_VERSION_8_6, STUDY_VERSION_8_8 +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import ( + STStorage880Properties, + 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 @@ -83,7 +86,7 @@ def test_init(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, pmax_injection=pmax_injection.tolist(), # type: ignore inflows=inflows.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, @@ -96,7 +99,7 @@ def test_init(self, command_context: CommandContext): 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 == STStorage880Properties(**expected_parameters) # check the matrices links @@ -114,23 +117,15 @@ def test_init__invalid_storage_name(self, recent_study: FileStudy, command_conte CreateSTStorage( command_context=command_context, area_id="dummy", - parameters=STStorageConfig(**parameters), + parameters=parameters, study_version=STUDY_VERSION_8_8, ) # We get 2 errors because the `storage_name` is duplicated in the `parameters`: assert ctx.value.error_count() == 1 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["type"] == "string_pattern_mismatch" + assert raised_error["msg"] == "String should match pattern '[a-zA-Z0-9_(),& -]+'" + assert raised_error["input"] == "?%$$" def test_init__invalid_matrix_values(self, command_context: CommandContext): array = GEN.random((8760, 1)) @@ -139,7 +134,7 @@ def test_init__invalid_matrix_values(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, pmax_injection=array.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, ) @@ -156,7 +151,7 @@ def test_init__invalid_matrix_shape(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, pmax_injection=array.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, ) @@ -173,7 +168,7 @@ def test_init__invalid_nan_value(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, pmax_injection=array.tolist(), # type: ignore study_version=STUDY_VERSION_8_8, ) @@ -188,7 +183,7 @@ def test_init__invalid_matrix_type(self, command_context: CommandContext): CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, pmax_injection=[1, 2, 3], study_version=STUDY_VERSION_8_8, ) @@ -198,24 +193,14 @@ def test_init__invalid_matrix_type(self, command_context: CommandContext): assert raised_error["msg"] == "Value error, Invalid matrix shape (3,), expected (8760, 1)" assert "pmax_injection" in raised_error["input"] - def test_apply_config__invalid_version(self, empty_study: FileStudy, command_context: CommandContext): - # Given an old study in version 720 - # When we apply the config to add a new ST Storage - create_st_storage = CreateSTStorage( - command_context=command_context, - area_id="foo", - parameters=STStorageConfig(**PARAMETERS), - study_version=empty_study.config.version, - ) - command_output = create_st_storage.apply_config(empty_study.config) - - # Then, the output should be an error - assert command_output.status is False - assert re.search( - rf"Invalid.*version {empty_study.config.version}", - command_output.message, - flags=re.IGNORECASE, - ) + def test_instantiate_with_invalid_version(self, empty_study: FileStudy, command_context: CommandContext): + with pytest.raises(ValidationError): + CreateSTStorage( + command_context=command_context, + area_id="foo", + parameters=PARAMETERS, + study_version=empty_study.config.version, + ) def test_apply_config__missing_area(self, recent_study: FileStudy, command_context: CommandContext): # Given a study without "unknown area" area @@ -223,7 +208,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=PARAMETERS, study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -247,7 +232,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=PARAMETERS, study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -258,7 +243,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=parameters, study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -282,7 +267,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=PARAMETERS, study_version=recent_study.config.version, ) command_output = create_st_storage.apply_config(recent_study.config) @@ -309,7 +294,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=PARAMETERS, pmax_injection=pmax_injection.tolist(), # type: ignore inflows=inflows.tolist(), # type: ignore study_version=recent_study.config.version, @@ -322,11 +307,11 @@ 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, - "name": "Storage1", + "name": "storage1", "reservoircapacity": 20000, "withdrawalnominalcapacity": 1500, } @@ -350,29 +335,12 @@ def test_apply__nominal_case(self, recent_study: FileStudy, command_context: Com } assert config == expected - def test_apply__invalid_apply_config(self, empty_study: FileStudy, command_context: CommandContext): - # First, prepare a new Area - create_area = CreateArea( - area_name="Area FR", command_context=command_context, study_version=empty_study.config.version - ) - create_area.apply(empty_study) - - # Then, apply the command to create a new ST Storage - cmd = CreateSTStorage( - command_context=command_context, - area_id=transform_name_to_id(create_area.area_name), - parameters=STStorageConfig(**PARAMETERS), - study_version=empty_study.config.version, - ) - command_output = cmd.apply(empty_study) - assert not command_output.status # invalid study (too old) - # noinspection SpellCheckingInspection def test_to_dto(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, study_version=STUDY_VERSION_8_8, ) @@ -381,6 +349,10 @@ 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["name"] = expected_parameters["name"].lower() + expected_parameters["group"] = expected_parameters["group"].lower() + # as we're using study version 8.8, we have the parameter `enabled` + expected_parameters["enabled"] = True constants = command_context.generator_matrix_constants assert actual == CommandDTO( @@ -401,7 +373,7 @@ def test_match_signature(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, study_version=STUDY_VERSION_8_8, ) assert cmd.match_signature() == "create_st_storage%area_fr%storage1" @@ -417,16 +389,16 @@ def test_match( cmd1 = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), - study_version=STUDY_VERSION_8_8, + parameters=PARAMETERS, + study_version=STUDY_VERSION_8_6, ) cmd2 = CreateSTStorage( command_context=command_context, area_id=area_id, - parameters=STStorageConfig(**parameters), - study_version=STUDY_VERSION_8_8, + parameters=parameters, + study_version=STUDY_VERSION_8_6, ) - light_equal = area_id == cmd1.area_id and parameters["name"] == cmd1.storage_name + light_equal = area_id == cmd1.area_id and parameters["name"].lower() == cmd1.storage_name assert cmd1.match(cmd2, equal=False) == light_equal deep_equal = area_id == cmd1.area_id and parameters == PARAMETERS assert cmd1.match(cmd2, equal=True) == deep_equal @@ -435,7 +407,7 @@ def test_match__unknown_type(self, command_context: CommandContext): cmd1 = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, study_version=STUDY_VERSION_8_8, ) # Always `False` when compared to another object type @@ -446,38 +418,41 @@ def test_create_diff__not_equals(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), - study_version=STUDY_VERSION_8_8, + parameters=PARAMETERS, + study_version=STUDY_VERSION_8_6, ) upper_rule_curve = GEN.random((8760, 1)) inflows = GEN.uniform(0, 1000, size=(8760, 1)) other = CreateSTStorage( command_context=command_context, area_id=cmd.area_id, - parameters=STStorageConfig(**OTHER_PARAMETERS), + parameters=OTHER_PARAMETERS, upper_rule_curve=upper_rule_curve.tolist(), # type: ignore inflows=inflows.tolist(), # type: ignore - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_6, ) actual = cmd.create_diff(other) + expected_params = copy.deepcopy(OTHER_PARAMETERS) + expected_params["name"] = expected_params["name"].lower() + expected_params["group"] = expected_params["group"].lower() expected = [ ReplaceMatrix( command_context=command_context, target="input/st-storage/series/area_fr/storage1/upper_rule_curve", matrix=strip_matrix_protocol(other.upper_rule_curve), - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_6, ), ReplaceMatrix( command_context=command_context, target="input/st-storage/series/area_fr/storage1/inflows", matrix=strip_matrix_protocol(other.inflows), - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_6, ), UpdateConfig( command_context=command_context, target="input/st-storage/clusters/area_fr/list/storage1", - data=OTHER_PARAMETERS, - study_version=STUDY_VERSION_8_8, + data=expected_params, + study_version=STUDY_VERSION_8_6, ), ] assert actual == expected @@ -486,7 +461,7 @@ def test_create_diff__equals(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=PARAMETERS, study_version=STUDY_VERSION_8_8, ) actual = cmd.create_diff(cmd) @@ -496,7 +471,7 @@ def test_get_inner_matrices(self, command_context: CommandContext): cmd = CreateSTStorage( command_context=command_context, area_id="area_fr", - parameters=STStorageConfig(**PARAMETERS), + parameters=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 3751c9f865..0a9ff4eff3 100644 --- a/tests/variantstudy/model/command/test_manage_binding_constraints.py +++ b/tests/variantstudy/model/command/test_manage_binding_constraints.py @@ -21,7 +21,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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_extractor import CommandExtractor from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter diff --git a/tests/variantstudy/model/command/test_manage_district.py b/tests/variantstudy/model/command/test_manage_district.py index b99d2ac380..4f1afe0652 100644 --- a/tests/variantstudy/model/command/test_manage_district.py +++ b/tests/variantstudy/model/command/test_manage_district.py @@ -13,8 +13,8 @@ from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.ini_reader import IniReader +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id 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.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/model/command/test_remove_area.py b/tests/variantstudy/model/command/test_remove_area.py index 468b468e69..aaf94ebe54 100644 --- a/tests/variantstudy/model/command/test_remove_area.py +++ b/tests/variantstudy/model/command/test_remove_area.py @@ -18,7 +18,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.field_validators 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_binding_constraint import CreateBindingConstraint diff --git a/tests/variantstudy/model/command/test_remove_cluster.py b/tests/variantstudy/model/command/test_remove_cluster.py index 2b30616a0a..cd96bacc6f 100644 --- a/tests/variantstudy/model/command/test_remove_cluster.py +++ b/tests/variantstudy/model/command/test_remove_cluster.py @@ -19,7 +19,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.field_validators 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_binding_constraint import CreateBindingConstraint @@ -37,7 +37,7 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> area_name = "Area_name" area_id = transform_name_to_id(area_name) cluster_name = "Cluster Name" - cluster_id = transform_name_to_id(cluster_name, lower=False) + cluster_id = transform_name_to_id(cluster_name) study_version = empty_study.config.version @@ -57,10 +57,10 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> cluster_name=cluster_name, parameters={ "group": "group", - "unitcount": "unitcount", - "nominalcapacity": "nominalcapacity", - "marginal-cost": "marginal-cost", - "market-bid-cost": "market-bid-cost", + "unitcount": 4, + "nominalcapacity": 1.2, + "marginal-cost": 1.2, + "market-bid-cost": 1.2, }, command_context=command_context, prepro=[[0]], diff --git a/tests/variantstudy/model/command/test_remove_link.py b/tests/variantstudy/model/command/test_remove_link.py index 832d8db20b..8dd0e17d07 100644 --- a/tests/variantstudy/model/command/test_remove_link.py +++ b/tests/variantstudy/model/command/test_remove_link.py @@ -22,8 +22,8 @@ from pydantic import ValidationError from antarest.study.model import STUDY_VERSION_8_8 +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id 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.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 @@ -98,7 +98,7 @@ def test_apply(self, tmpdir: Path, command_context: CommandContext, version: int study_version = empty_study.config.version # Create some areas - areas = {transform_name_to_id(area, lower=True): area for area in ["Area_X", "Area_Y", "Area_Z"]} + areas = {transform_name_to_id(area): area for area in ["Area_X", "Area_Y", "Area_Z"]} for area in areas.values(): output = CreateArea(area_name=area, command_context=command_context, study_version=study_version).apply( empty_study diff --git a/tests/variantstudy/model/command/test_remove_renewables_cluster.py b/tests/variantstudy/model/command/test_remove_renewables_cluster.py index c2152a08b3..192c51cf30 100644 --- a/tests/variantstudy/model/command/test_remove_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_remove_renewables_cluster.py @@ -13,7 +13,8 @@ from checksumdir import dirhash from antarest.study.model import 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.field_validators 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.create_renewables_cluster import CreateRenewablesCluster @@ -32,7 +33,7 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext) -> area_name = "Area_name" area_id = transform_name_to_id(area_name) cluster_name = "Cluster Name" - cluster_id = transform_name_to_id(cluster_name, lower=False) + cluster_id = transform_name_to_id(cluster_name) output = CreateArea(area_name=area_name, 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 01c74235dd..41248405d1 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.field_validators 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 @@ -72,7 +72,7 @@ def test_init(self, command_context: CommandContext): assert cmd.area_id == "area_fr" assert cmd.storage_id == "storage_1" - def test_init__invalid_storage_id(self, recent_study: FileStudy, command_context: CommandContext): + def test_init__invalid_storage_id(self, command_context: CommandContext): # When we apply the config for a new ST Storage with a bad name with pytest.raises(ValidationError) as ctx: RemoveSTStorage( @@ -81,16 +81,11 @@ def test_init__invalid_storage_id(self, recent_study: FileStudy, command_context storage_id="?%$$", # bad name study_version=STUDY_VERSION_8_8, ) - assert ctx.value.errors() == [ - { - "ctx": {"pattern": "[a-z0-9_(),& -]+"}, - "input": "?%$$", - "loc": ("storage_id",), - "msg": "String should match pattern '[a-z0-9_(),& -]+'", - "type": "string_pattern_mismatch", - "url": "https://errors.pydantic.dev/2.8/v/string_pattern_mismatch", - } - ] + assert len(ctx.value.errors()) == 1 + error = ctx.value.errors()[0] + assert error["type"] == "string_pattern_mismatch" + assert error["loc"] == ("storage_id",) + assert error["msg"] == "String should match pattern '[a-z0-9_(),& -]+'" def test_apply_config__invalid_version(self, empty_study: FileStudy, command_context: CommandContext): # Given an old study in version 720 diff --git a/tests/variantstudy/model/command/test_replace_matrix.py b/tests/variantstudy/model/command/test_replace_matrix.py index 5f676ded29..5af3368821 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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/model/command/test_update_config.py b/tests/variantstudy/model/command/test_update_config.py index 1857602cb2..50c504f81f 100644 --- a/tests/variantstudy/model/command/test_update_config.py +++ b/tests/variantstudy/model/command/test_update_config.py @@ -18,7 +18,7 @@ from antarest.core.exceptions import ChildNotFoundError from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.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.field_validators import transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.business.command_reverter import CommandReverter from antarest.study.storage.variantstudy.model.command.create_area import CreateArea diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index c4406b50da..c1ed573009 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -19,7 +19,7 @@ import pytest from antarest.matrixstore.service import MatrixService -from antarest.study.model import STUDY_VERSION_8_8 +from antarest.study.model import STUDY_VERSION_8_2, STUDY_VERSION_8_6, STUDY_VERSION_8_8 from antarest.study.storage.patch_service import PatchService from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants from antarest.study.storage.variantstudy.command_factory import CommandFactory @@ -156,26 +156,6 @@ 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.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", - }, - "prepro": "prepro", - "modulation": "modulation", - }, - study_version=STUDY_VERSION_8_8, - ), CommandDTO( action=CommandName.CREATE_THERMAL_CLUSTER.value, args=[ @@ -183,17 +163,33 @@ "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", + "co2": 0.0, + "enabled": True, + "fixed-cost": 0.0, + "gen-ts": "use global", + "group": "other 1", + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 2.0, + "market-bid-cost": 2.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, + "must-run": False, + "name": "cluster_name", + "nominalcapacity": 2.0, + "spinning": 0.0, + "spread-cost": 0.0, + "startup-cost": 0.0, + "unitcount": 2, + "volatility.forced": 0.0, + "volatility.planned": 0.0, }, "prepro": "prepro", "modulation": "modulation", } ], - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_2, ), CommandDTO( action=CommandName.REMOVE_THERMAL_CLUSTER.value, @@ -211,26 +207,16 @@ "area_id": "area_name", "cluster_name": "cluster_name", "parameters": { + "enabled": True, + "group": "other res 1", "name": "name", + "nominalcapacity": 0.0, "ts-interpretation": "power-generation", + "unitcount": 1, }, }, study_version=STUDY_VERSION_8_8, ), - CommandDTO( - action=CommandName.CREATE_RENEWABLES_CLUSTER.value, - args=[ - { - "area_id": "area_name", - "cluster_name": "cluster_name", - "parameters": { - "name": "name", - "ts-interpretation": "power-generation", - }, - } - ], - study_version=STUDY_VERSION_8_8, - ), CommandDTO( action=CommandName.REMOVE_RENEWABLES_CLUSTER.value, args={"area_id": "area_name", "cluster_id": "cluster_name"}, @@ -314,8 +300,8 @@ args={ "area_id": "area 1", "parameters": { - "name": "Storage 1", - "group": "Battery", + "name": "storage 1", + "group": "battery", "injectionnominalcapacity": 0, "withdrawalnominalcapacity": 0, "reservoircapacity": 0, @@ -329,7 +315,7 @@ "upper_rule_curve": "matrix://8ce614c8-c687-41af-8b24-df8a49cc52af", "inflows": "matrix://df9b25e1-e3f7-4a57-8182-0ff9791439e5", }, - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_6, ), CommandDTO( action=CommandName.CREATE_ST_STORAGE.value, @@ -338,11 +324,11 @@ "area_id": "area 1", "parameters": { "efficiency": 1, - "group": "Battery", + "group": "battery", "initiallevel": 0, "initialleveloptim": False, "injectionnominalcapacity": 0, - "name": "Storage 1", + "name": "storage 1", "reservoircapacity": 0, "withdrawalnominalcapacity": 0, }, @@ -356,11 +342,11 @@ "area_id": "area 1", "parameters": { "efficiency": 0.94, - "group": "Battery", + "group": "battery", "initiallevel": 0, "initialleveloptim": False, "injectionnominalcapacity": 0, - "name": "Storage 2", + "name": "storage 2", "reservoircapacity": 0, "withdrawalnominalcapacity": 0, }, @@ -371,7 +357,7 @@ "inflows": "matrix://e8923768-9bdd-40c2-a6ea-2da2523be727", }, ], - study_version=STUDY_VERSION_8_8, + study_version=STUDY_VERSION_8_6, ), CommandDTO( action=CommandName.REMOVE_ST_STORAGE.value, 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 0ee371d7fa..8983dde2e8 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 @@ -47,6 +47,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 a180b0fe05..5c3e62b81f 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 @@ -81,7 +81,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 f3e2520aa2..8ad72eeffc 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 @@ -26,15 +26,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 = [ 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 62f351772a..e3cacb065d 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 @@ -49,6 +49,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 22899502df..4ee29080d4 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 83bd3c60f7..3d05b9a659 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 3aaf182b48..7d3d5bb0de 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 @@ -59,6 +59,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 3a5895c8f6..6747954f6b 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 @@ -42,16 +42,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 b6b95b14169c50e497003c13007f40d90220c962 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Mon, 6 Jan 2025 15:19:12 +0100 Subject: [PATCH 02/14] add integration test to replicate Thibault bug --- .../study_data_blueprint/test_lower_case.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/integration/study_data_blueprint/test_lower_case.py diff --git a/tests/integration/study_data_blueprint/test_lower_case.py b/tests/integration/study_data_blueprint/test_lower_case.py new file mode 100644 index 0000000000..7acd48c55a --- /dev/null +++ b/tests/integration/study_data_blueprint/test_lower_case.py @@ -0,0 +1,88 @@ +# +# 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 copy +from pathlib import Path + +import pytest +from starlette.testclient import TestClient + +from antarest.study.storage.rawstudy.ini_reader import IniReader +from antarest.study.storage.rawstudy.ini_writer import IniWriter +from tests.integration.prepare_proxy import PreparerProxy + + +class TestLowerCase: + """ + Checks the app read and writes several fields in lower-case and handle all cases correctly + """ + + @pytest.mark.parametrize("cluster_type", ["thermal", "renewables", "st-storage"]) + def test_clusters(self, client: TestClient, user_access_token: str, tmp_path: Path, cluster_type: str) -> None: + # Study preparation + client.headers = {"Authorization": f"Bearer {user_access_token}"} + preparer = PreparerProxy(client, user_access_token) + study_id = preparer.create_study("foo", version=880) + study_path = tmp_path / "internal_workspace" / study_id + area1_id = preparer.create_area(study_id, name="Area 1")["id"] + + # Creates the cluster + if cluster_type == "thermal": + cluster_grp = "Nuclear" + url = "clusters/thermal" + elif cluster_type == "renewables": + cluster_grp = "Solar Thermal" + url = "clusters/renewable" + else: + cluster_grp = "Battery" + url = "storages" + cluster_name = "Cluster 1" + lowered_name = cluster_name.lower() + lowered_grp = cluster_grp.lower() + res = client.post( + f"/v1/studies/{study_id}/areas/{area1_id}/{url}", json={"name": cluster_name, "group": cluster_grp} + ) + assert res.status_code == 200, res.json() + + # Asserts the fields are written in lower case inside the ini file + ini_path = study_path / "input" / cluster_type / "clusters" / area1_id / "list.ini" + ini_content = IniReader().read(ini_path) + assert list(ini_content.keys()) == [lowered_name] + assert ini_content[lowered_name]["group"] == lowered_grp + + # Rewrite the cluster name in MAJ to mimic legacy clusters + new_content = copy.deepcopy(ini_content) + new_content[cluster_name] = new_content.pop(lowered_name) + new_content[cluster_name]["name"] = cluster_name + new_content[cluster_name]["group"] = cluster_grp + IniWriter().write(new_content, ini_path) + + # Asserts the GET still works and returns the name in lower case + res = client.get(f"/v1/studies/{study_id}/areas/{area1_id}/{url}/{cluster_name}") + cluster = res.json() + assert cluster["name"] == lowered_name + assert cluster["group"] == lowered_grp + + # Try to update a property + if cluster_type == "st-storage": + params = {"efficiency": 0.8} + else: + params = {"nominalCapacity": 2} + res = client.patch( + f"/v1/studies/{study_id}/areas/{area1_id}/{url}/{cluster_name}", json={"name": cluster_name, **params} + ) + assert res.status_code == 200, res.json() + + # We shouldn't create a 2nd cluster, but rather update the first one + res = client.get(f"/v1/studies/{study_id}/areas/{area1_id}/{url}") + assert res.status_code == 200, res.json() + clusters = res.json() + assert len(clusters) == 1 + assert clusters[0][list(params.keys())[0]] == list(params.values())[0] From 24ebeaaab27a53390df901786e6d289337a722d6 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 7 Jan 2025 13:44:00 +0100 Subject: [PATCH 03/14] continue work --- .../bindingconstraints_ini.py | 22 ++++++++++ .../study_data_blueprint/test_lower_case.py | 42 +++++++++++++++++++ 2 files changed, 64 insertions(+) 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..29b7a4e901 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,10 +9,15 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import typing as t +from typing_extensions import override + +from antarest.core.model import SUB_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.ini_file_node import IniFileNode +from study.storage.rawstudy.model.filesystem.inode import INode # noinspection SpellCheckingInspection @@ -35,3 +40,20 @@ class BindingConstraintsIni(IniFileNode): def __init__(self, context: ContextServer, config: FileStudyTreeConfig): super().__init__(context, config, types={}) + + @override + def get( + self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True + ) -> SUB_JSON: + output = self._get(url, depth, expanded, get_node=False) + assert not isinstance(output, INode) + # We need to lower the group attribute + for key, bc in output.items(): + if "group" in bc: + bc["group"] = bc["group"].lower() + return output + + @override + def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: + new_data = data + super().save(new_data, url) diff --git a/tests/integration/study_data_blueprint/test_lower_case.py b/tests/integration/study_data_blueprint/test_lower_case.py index 7acd48c55a..25e76a5198 100644 --- a/tests/integration/study_data_blueprint/test_lower_case.py +++ b/tests/integration/study_data_blueprint/test_lower_case.py @@ -17,6 +17,7 @@ from antarest.study.storage.rawstudy.ini_reader import IniReader from antarest.study.storage.rawstudy.ini_writer import IniWriter from tests.integration.prepare_proxy import PreparerProxy +from tests.integration.utils import wait_task_completion class TestLowerCase: @@ -86,3 +87,44 @@ def test_clusters(self, client: TestClient, user_access_token: str, tmp_path: Pa clusters = res.json() assert len(clusters) == 1 assert clusters[0][list(params.keys())[0]] == list(params.values())[0] + + def test_constraints(self, client: TestClient, user_access_token: str, tmp_path: Path) -> None: + # Study preparation + client.headers = {"Authorization": f"Bearer {user_access_token}"} + preparer = PreparerProxy(client, user_access_token) + study_id = preparer.create_study("foo", version=880) + study_id = preparer.create_variant(study_id, name="variant_1") + study_path = tmp_path / "internal_workspace" / study_id / "snapshot" + + # Create the Binding Constraint with a group in MAJ + bc_group = "Group 1" + lowered_group = bc_group.lower() + bc_1 = preparer.create_binding_constraint(study_id, name="bc_1", group=bc_group) + assert bc_1["group"] == lowered_group + + # Generates the variant + task_id = client.put(f"/v1/studies/{study_id}/generate") + wait_task_completion(client, user_access_token, task_id.json()) + + # Asserts the field is in lower case in the INI file + ini_path = study_path / "input" / "bindingconstraints" / "bindingconstraints.ini" + ini_content = IniReader().read(ini_path) + assert ini_content["0"]["group"] == lowered_group + + # Writes the group in MAJ to mimic legacy constraint + new_content = copy.deepcopy(ini_content) + new_content["0"]["group"] = bc_group + IniWriter().write(new_content, ini_path) + + # Asserts the group is read as lower case + res = client.get(f"/v1/studies/{study_id}/bindingconstraints") + assert res.status_code == 200, res.json() + bcs = res.json() + assert len(bcs) == 1 + assert bcs[0]["group"] == lowered_group + + res = client.get(f"/v1/studies/{study_id}/raw?path=input/bindingconstraints/bindingconstraints") + assert res.status_code == 200, res.json() + bcs = res.json() + assert len(bcs) == 1 + assert bcs["0"]["group"] == lowered_group From 3be9a85c56095cf79b9c57eabd4d707dbff00448 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 8 Jan 2025 10:56:33 +0100 Subject: [PATCH 04/14] implement get and save methods inside nodes --- .../model/filesystem/ini_file_node.py | 48 +++++++++++++++++++ .../bindingconstraints_ini.py | 15 +----- .../root/input/renewables/clusters.py | 15 +++++- .../root/input/thermal/cluster/area/list.py | 15 ++++++ 4 files changed, 79 insertions(+), 14 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 960ccc83e4..5182652c1d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -325,3 +325,51 @@ def _validate_param( if raising: raise ValueError(msg) errors.append(msg) + + def get_lowered_content( + self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False + ) -> SUB_JSON: + output = self._get(url, depth, expanded, get_node=False) + assert not isinstance(output, INode) + if depth <= -1 and expanded: + return output + + # We need to lower the group attribute and the cluster ids + if not url: + assert isinstance(output, dict) + for key in list(output.keys()): + new_key = str(key).lower() + output[new_key] = output.pop(key) + if "group" in output[new_key]: + output[new_key]["group"] = str(output[new_key]["group"]).lower() + elif len(url) == 1: + assert isinstance(output, dict) + if "group" in output: + output["group"] = str(output["group"]).lower() + elif len(url) == 2 and url[1] == "group": + output = str(output).lower() + return output + + def save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: + self._assert_not_in_zipped_file() + with FileLock( + str( + Path(tempfile.gettempdir()) + / f"{self.config.study_id}-{self.path.relative_to(self.config.study_path).name.replace(os.sep, '.')}.lock" + ) + ): + info = self.get_lowered_content([]) # We read the cluster ids in lower case + assert isinstance(info, dict) + obj = data + if isinstance(data, str): + with contextlib.suppress(pydantic_core.ValidationError): + obj = from_json(data) + if len(url) == 2: + if str(url[0]).lower() not in info: + info[url[0]] = {} + info[url[0]][url[1]] = obj + elif len(url) == 1: + info[str(url[0]).lower()] = obj + else: + info = t.cast(JSON, obj) + self.writer.write(info, self.path) 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 29b7a4e901..30148a4bbc 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,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. + import typing as t from typing_extensions import override @@ -17,7 +18,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 -from study.storage.rawstudy.model.filesystem.inode import INode # noinspection SpellCheckingInspection @@ -45,15 +45,4 @@ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - output = self._get(url, depth, expanded, get_node=False) - assert not isinstance(output, INode) - # We need to lower the group attribute - for key, bc in output.items(): - if "group" in bc: - bc["group"] = bc["group"].lower() - return output - - @override - def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - new_data = data - super().save(new_data, url) + return super().get_lowered_content(url, depth, expanded) 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..c6ea442c10 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 @@ -9,8 +9,11 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import typing as t + from typing_extensions import override +from antarest.core.model import SUB_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.folder_node import FolderNode @@ -30,12 +33,22 @@ def __init__( "group": str, "enabled": bool, "unitcount": int, - "nomialcapacity": 0, + "nomialcapacity": float, "ts-interpretation": str, } types = {cluster_id: section for cluster_id in config.get_renewable_ids(area)} IniFileNode.__init__(self, context, config, types) + @override + def get( + self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True + ) -> SUB_JSON: + return super().get_lowered_content(url, depth, expanded) + + @override + def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: + super().save_lowered_content(data, url or []) + class ClusteredRenewableCluster(FolderNode): def __init__( 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..5bed2db83f 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 @@ -10,6 +10,11 @@ # # This file is part of the Antares project. +import typing as t + +from typing_extensions import override + +from antarest.core.model import SUB_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.ini_file_node import IniFileNode @@ -31,3 +36,13 @@ def __init__( } types = {th: section for th in config.get_thermal_ids(area)} super().__init__(context, config, types) + + @override + def get( + self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True + ) -> SUB_JSON: + return super().get_lowered_content(url, depth, expanded) + + @override + def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: + super().save_lowered_content(data, url or []) From 2d181644a429b2895013d6d563abb5653127e039 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 8 Jan 2025 10:57:33 +0100 Subject: [PATCH 05/14] fix license headers issue --- tests/integration/study_data_blueprint/test_lower_case.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/integration/study_data_blueprint/test_lower_case.py b/tests/integration/study_data_blueprint/test_lower_case.py index 25e76a5198..8bf1684c55 100644 --- a/tests/integration/study_data_blueprint/test_lower_case.py +++ b/tests/integration/study_data_blueprint/test_lower_case.py @@ -1,3 +1,4 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) # # See AUTHORS.txt # @@ -8,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. + import copy from pathlib import Path From eb7c956c95a52ab4cbb501d7ab12f11b729477aa Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 8 Jan 2025 11:10:31 +0100 Subject: [PATCH 06/14] update test --- .../integration/study_data_blueprint/test_lower_case.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/integration/study_data_blueprint/test_lower_case.py b/tests/integration/study_data_blueprint/test_lower_case.py index 8bf1684c55..274e042ce8 100644 --- a/tests/integration/study_data_blueprint/test_lower_case.py +++ b/tests/integration/study_data_blueprint/test_lower_case.py @@ -70,9 +70,16 @@ def test_clusters(self, client: TestClient, user_access_token: str, tmp_path: Pa # Asserts the GET still works and returns the name in lower case res = client.get(f"/v1/studies/{study_id}/areas/{area1_id}/{url}/{cluster_name}") cluster = res.json() - assert cluster["name"] == lowered_name + assert cluster["name"] == cluster_name assert cluster["group"] == lowered_grp + # Also checks the GET /raw endpoint + res = client.get(f"/v1/studies/{study_id}/raw?path=input/{cluster_type}/clusters/{area1_id}/list") + cluster_list = res.json() + assert list(cluster_list.keys()) == [lowered_name] + assert cluster_list[lowered_name]["name"] == cluster_name + assert cluster_list[lowered_name]["group"] == lowered_grp + # Try to update a property if cluster_type == "st-storage": params = {"efficiency": 0.8} From 164f84813e71c6984e5f0ad9f6ae04774831d859 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 8 Jan 2025 13:42:04 +0100 Subject: [PATCH 07/14] put back cluster names in maj --- .../model/filesystem/config/cluster.py | 4 ++-- .../model/filesystem/ini_file_node.py | 14 +++++------ .../input/st_storage/clusters/area/list.py | 14 +++++++++++ .../model/command/create_cluster.py | 4 ++-- .../command/create_renewables_cluster.py | 4 ++-- antarest/study/web/study_data_blueprint.py | 2 +- .../test_binding_constraints.py | 2 +- .../study_data_blueprint/test_renewable.py | 6 ++--- .../study_data_blueprint/test_st_storage.py | 23 +++++++++---------- .../study_data_blueprint/test_thermal.py | 8 +++---- .../test_renewable_cluster.py | 12 +++++----- .../areas/test_st_storage_management.py | 14 +++++------ .../business/areas/test_thermal_management.py | 4 ++-- .../model/command/test_create_cluster.py | 8 +++---- .../command/test_create_renewables_cluster.py | 10 ++++---- .../model/command/test_create_st_storage.py | 6 ++--- tests/variantstudy/test_command_factory.py | 4 ++-- 17 files changed, 75 insertions(+), 64 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py index f35eb13a3b..9ef20ad596 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/cluster.py @@ -50,7 +50,7 @@ class ItemProperties( group: LowerCaseStr = Field(default="", description="Cluster group") - name: LowerCaseStr = Field(description="Cluster name", pattern=r"[a-zA-Z0-9_(),& -]+") + name: str = Field(description="Cluster name", pattern=r"[a-zA-Z0-9_(),& -]+") def __lt__(self, other: t.Any) -> bool: """ @@ -59,7 +59,7 @@ def __lt__(self, other: t.Any) -> bool: This method may be used to sort and group clusters by `group` and `name`. """ if isinstance(other, ItemProperties): - return (self.group, self.name).__lt__((other.group, other.name)) + return (self.group, self.name.lower()).__lt__((other.group, other.name.lower())) return NotImplemented diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 5182652c1d..9c339bebdd 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -364,12 +364,12 @@ def save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: if isinstance(data, str): with contextlib.suppress(pydantic_core.ValidationError): obj = from_json(data) - if len(url) == 2: - if str(url[0]).lower() not in info: - info[url[0]] = {} - info[url[0]][url[1]] = obj - elif len(url) == 1: - info[str(url[0]).lower()] = obj - else: + if len(url) not in {1, 2}: info = t.cast(JSON, obj) + else: + lowered_id = str(url[0]).lower() # We lower the given cluster id + if len(url) == 2: + info.setdefault(lowered_id, {})[url[1]] = obj + else: + info[lowered_id] = obj self.writer.write(info, self.path) 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..eea6c22c6f 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,7 +9,11 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import typing as t +from typing_extensions import override + +from antarest.core.model import SUB_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.ini_file_node import IniFileNode @@ -34,3 +38,13 @@ def __init__( # - 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) + + @override + def get( + self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True + ) -> SUB_JSON: + return super().get_lowered_content(url, depth, expanded) + + @override + def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: + super().save_lowered_content(data, url or []) diff --git a/antarest/study/storage/variantstudy/model/command/create_cluster.py b/antarest/study/storage/variantstudy/model/command/create_cluster.py index 848c200466..96b3a9596f 100644 --- a/antarest/study/storage/variantstudy/model/command/create_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_cluster.py @@ -15,7 +15,7 @@ from pydantic import Field, model_validator from typing_extensions import override -from antarest.core.model import JSON, LowerCaseStr +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 @@ -49,7 +49,7 @@ class CreateCluster(ICommand): # ================== area_id: str - cluster_name: LowerCaseStr + cluster_name: str parameters: ThermalPropertiesType prepro: t.Optional[t.Union[t.List[t.List[MatrixData]], str]] = Field(None, validate_default=True) modulation: t.Optional[t.Union[t.List[t.List[MatrixData]], str]] = Field(None, validate_default=True) 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 342bf32690..d33517f77f 100644 --- a/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py +++ b/antarest/study/storage/variantstudy/model/command/create_renewables_cluster.py @@ -15,7 +15,7 @@ from pydantic import model_validator from typing_extensions import override -from antarest.core.model import JSON, LowerCaseStr +from antarest.core.model import JSON from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, EnrModelling, FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.config.renewable import ( RenewableProperties, @@ -44,7 +44,7 @@ class CreateRenewablesCluster(ICommand): # ================== area_id: str - cluster_name: LowerCaseStr + cluster_name: str parameters: RenewableProperties @model_validator(mode="before") diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index b535a0526f..eaba4a262b 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -2578,7 +2578,7 @@ def duplicate_cluster( area_id: str, cluster_type: ClusterType, source_cluster_id: LowerCaseStr, - new_cluster_name: LowerCaseStr = Query(..., alias="newName", title="New Cluster Name"), + new_cluster_name: str = Query(..., alias="newName", title="New Cluster Name"), current_user: JWTUser = Depends(auth.get_current_user), ) -> t.Union[STStorageOutput, ThermalClusterOutput, RenewableClusterOutput]: logger.info( diff --git a/tests/integration/study_data_blueprint/test_binding_constraints.py b/tests/integration/study_data_blueprint/test_binding_constraints.py index a31ab622dd..b91b6d0509 100644 --- a/tests/integration/study_data_blueprint/test_binding_constraints.py +++ b/tests/integration/study_data_blueprint/test_binding_constraints.py @@ -192,7 +192,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st clusters_list = preparer.get_thermals(study_id, area1_id) assert len(clusters_list) == 1 assert clusters_list[0]["id"] == cluster_id - assert clusters_list[0]["name"] == "cluster 1" + assert clusters_list[0]["name"] == "Cluster 1" assert clusters_list[0]["group"] == "nuclear" if study_type == "variant": diff --git a/tests/integration/study_data_blueprint/test_renewable.py b/tests/integration/study_data_blueprint/test_renewable.py index d14ceaa47a..bbef4bba8a 100644 --- a/tests/integration/study_data_blueprint/test_renewable.py +++ b/tests/integration/study_data_blueprint/test_renewable.py @@ -134,7 +134,7 @@ def test_lifecycle( fr_solar_pv_cfg = { "id": fr_solar_pv_id, **fr_solar_pv_props, - "name": fr_solar_pv.lower(), + "name": fr_solar_pv, "group": fr_solar_pv_props["group"].lower(), } assert res.json() == fr_solar_pv_cfg @@ -260,7 +260,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["name"] = new_name.lower() + duplicated_config["name"] = new_name duplicated_id = transform_name_to_id(new_name) duplicated_config["id"] = duplicated_id assert res.json() == duplicated_config @@ -586,7 +586,7 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var ) assert res.status_code in {200, 201}, res.json() cluster_cfg = res.json() - assert cluster_cfg["name"] == new_name.lower() + assert cluster_cfg["name"] == new_name new_id = cluster_cfg["id"] # Check that the duplicate has the right properties diff --git a/tests/integration/study_data_blueprint/test_st_storage.py b/tests/integration/study_data_blueprint/test_st_storage.py index 21329e4c88..39f7c153df 100644 --- a/tests/integration/study_data_blueprint/test_st_storage.py +++ b/tests/integration/study_data_blueprint/test_st_storage.py @@ -165,7 +165,7 @@ def test_lifecycle__nominal( **default_output, **siemens_properties, "id": siemens_battery_id, - "name": siemens_battery.lower(), + "name": siemens_battery, } assert res.json() == siemens_output @@ -238,7 +238,7 @@ def test_lifecycle__nominal( assert res.status_code == 200, res.json() siemens_output = { **siemens_output, - "name": "new siemens battery", + "name": "New Siemens Battery", "reservoirCapacity": 2500, } assert res.json() == siemens_output @@ -304,7 +304,7 @@ def test_lifecycle__nominal( assert res.status_code in {200, 201}, res.json() # asserts the config is the same duplicated_output = dict(siemens_output) - duplicated_output["name"] = new_name.lower() + duplicated_output["name"] = new_name duplicated_id = transform_name_to_id(new_name) duplicated_output["id"] = duplicated_id assert res.json() == duplicated_output @@ -389,10 +389,9 @@ 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} - # assert we return name and group as lower values - for key in ["name", "group"]: - grand_maison_output[key] = grand_maison_properties[key].lower() - siemens_output[key] = siemens_properties[key].lower() + # assert we return group in lower case + grand_maison_output["group"] = grand_maison_properties["group"].lower() + siemens_output["group"] = siemens_properties["group"].lower() assert res.json() == [duplicated_output, siemens_output, grand_maison_output] # We can delete the three short-term storages at once. @@ -620,7 +619,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.lower(), "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 @@ -631,7 +630,7 @@ def test__default_values( ) assert res.status_code == 200, res.json() actual = res.json() - expected = {**default_config, "name": tesla_battery.lower(), "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. @@ -661,7 +660,7 @@ def test__default_values( "action": "create_st_storage", "args": { "area_id": "fr", - "parameters": {**default_config, "name": siemens_battery.lower(), "group": "battery"}, + "parameters": {**default_config, "name": siemens_battery, "group": "battery"}, "pmax_injection": ANY, "pmax_withdrawal": ANY, "lower_rule_curve": ANY, @@ -742,7 +741,7 @@ def test__default_values( actual = res.json() expected = { **default_config, - "name": siemens_battery.lower(), + "name": siemens_battery, "group": "battery", "injectionnominalcapacity": 1600, "initiallevel": 0.0, @@ -828,7 +827,7 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var ) assert res.status_code in {200, 201}, res.json() cluster_cfg = res.json() - assert cluster_cfg["name"] == new_name.lower() + assert cluster_cfg["name"] == new_name new_id = cluster_cfg["id"] # Check that the duplicate has the right properties diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index 0ad7c7e881..7427e9a56f 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -380,7 +380,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st **fr_gas_conventional_props, "id": fr_gas_conventional_id, **{p: pollutants_values for p in pollutants_names}, - "name": fr_gas_conventional_props["name"].lower(), + "name": fr_gas_conventional_props["name"], "group": fr_gas_conventional_props["group"].lower(), } fr_gas_conventional_cfg = { @@ -439,7 +439,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.status_code == 200, res.json() fr_gas_conventional_cfg = { **fr_gas_conventional_cfg, - "name": name.lower(), + "name": name, "nominalCapacity": 32.1, } assert res.json() == fr_gas_conventional_cfg @@ -512,7 +512,7 @@ def test_lifecycle(self, client: TestClient, user_access_token: str, internal_st assert res.status_code in {200, 201}, res.json() # asserts the config is the same duplicated_config = dict(fr_gas_conventional_cfg) - duplicated_config["name"] = new_name.lower() + duplicated_config["name"] = new_name duplicated_id = transform_name_to_id(new_name) duplicated_config["id"] = duplicated_id # takes the update into account @@ -921,7 +921,7 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var ) assert res.status_code in {200, 201}, res.json() cluster_cfg = res.json() - assert cluster_cfg["name"] == new_name.lower() + assert cluster_cfg["name"] == new_name new_id = cluster_cfg["id"] # Check that the duplicate has the right properties diff --git a/tests/integration/variant_blueprint/test_renewable_cluster.py b/tests/integration/variant_blueprint/test_renewable_cluster.py index d5c8154875..653cf02bc0 100644 --- a/tests/integration/variant_blueprint/test_renewable_cluster.py +++ b/tests/integration/variant_blueprint/test_renewable_cluster.py @@ -126,7 +126,7 @@ def test_lifecycle( "enabled": True, "group": "wind offshore", "id": "oleron", - "name": cluster_fr1.lower(), + "name": cluster_fr1, "nominalCapacity": 2500.0, "tsInterpretation": "power-generation", "unitCount": 1, @@ -143,7 +143,7 @@ def test_lifecycle( "enabled": False, "group": "solar pv", "id": "la_rochelle", - "name": cluster_fr2.lower(), + "name": cluster_fr2, "nominalCapacity": 3500.0, "tsInterpretation": "power-generation", "unitCount": 4, @@ -230,7 +230,7 @@ def test_lifecycle( "enabled": True, "group": "wind offshore", "id": "ol ron", - "name": cluster_it1.lower(), + "name": cluster_it1, "nominalCapacity": 1000.0, "tsInterpretation": "production-factor", "unitCount": 1, @@ -274,7 +274,7 @@ def test_lifecycle( "list": { cluster_fr1_id: { "group": "wind offshore", - "name": cluster_fr1.lower(), + "name": cluster_fr1, "nominalcapacity": 2500, "ts-interpretation": "power-generation", "unitcount": 1, @@ -286,7 +286,7 @@ def test_lifecycle( "list": { cluster_it1_id: { "group": "wind offshore", - "name": cluster_it1.lower(), + "name": cluster_it1, "nominalcapacity": 1000, "ts-interpretation": "production-factor", "unitcount": 1, @@ -320,7 +320,7 @@ def test_lifecycle( "list": { cluster_it1_id: { "group": "wind offshore", - "name": cluster_it1.lower(), + "name": cluster_it1, "nominalcapacity": 1000, "ts-interpretation": "production-factor", "unitcount": 1, diff --git a/tests/study/business/areas/test_st_storage_management.py b/tests/study/business/areas/test_st_storage_management.py index 4f22df97b2..8ce3b3c910 100644 --- a/tests/study/business/areas/test_st_storage_management.py +++ b/tests/study/business/areas/test_st_storage_management.py @@ -158,7 +158,7 @@ def test_get_all_storages__nominal_case( "id": "storage1", "enabled": None, "group": STStorageGroup.BATTERY, - "name": "storage1", + "name": "Storage1", "injectionNominalCapacity": 1500.0, "withdrawalNominalCapacity": 1500.0, "reservoirCapacity": 20000.0, @@ -170,7 +170,7 @@ def test_get_all_storages__nominal_case( "id": "storage2", "enabled": None, "group": STStorageGroup.PSP_CLOSED, - "name": "storage2", + "name": "Storage2", "injectionNominalCapacity": 2000.0, "withdrawalNominalCapacity": 1500.0, "reservoirCapacity": 20000.0, @@ -182,7 +182,7 @@ def test_get_all_storages__nominal_case( "id": "storage3", "enabled": None, "group": STStorageGroup.PSP_CLOSED, - "name": "storage3", + "name": "Storage3", "injectionNominalCapacity": 1500.0, "withdrawalNominalCapacity": 1500.0, "reservoirCapacity": 21000.0, @@ -264,7 +264,7 @@ def test_get_st_storages__nominal_case( "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 1500.0, - "name": "storage1", + "name": "Storage1", "reservoirCapacity": 20000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, @@ -276,7 +276,7 @@ def test_get_st_storages__nominal_case( "initialLevel": 0.5, "initialLevelOptim": False, "injectionNominalCapacity": 2000.0, - "name": "storage2", + "name": "Storage2", "reservoirCapacity": 20000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, @@ -288,7 +288,7 @@ def test_get_st_storages__nominal_case( "initialLevel": 1, "initialLevelOptim": False, "injectionNominalCapacity": 1500.0, - "name": "storage3", + "name": "Storage3", "reservoirCapacity": 21000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, @@ -375,7 +375,7 @@ def test_get_st_storage__nominal_case( "initialLevel": 0.5, "initialLevelOptim": True, "injectionNominalCapacity": 1500.0, - "name": "storage1", + "name": "Storage1", "reservoirCapacity": 20000.0, "withdrawalNominalCapacity": 1500.0, "enabled": None, diff --git a/tests/study/business/areas/test_thermal_management.py b/tests/study/business/areas/test_thermal_management.py index 64a351f7b2..de0ff4928b 100644 --- a/tests/study/business/areas/test_thermal_management.py +++ b/tests/study/business/areas/test_thermal_management.py @@ -382,7 +382,7 @@ def test_create_cluster__study_legacy( "minStablePower": 0.0, "minUpTime": 15, "mustRun": False, - "name": "new cluster", + "name": "New Cluster", "nh3": None, "nmvoc": None, "nominalCapacity": 1000.0, @@ -430,7 +430,7 @@ def test_update_cluster( expected = { "id": "2 avail and must 1", "group": ThermalClusterGroup.GAS, - "name": "new name", + "name": "New name", "enabled": False, "unitCount": 100, "nominalCapacity": 2000.0, diff --git a/tests/variantstudy/model/command/test_create_cluster.py b/tests/variantstudy/model/command/test_create_cluster.py index 8f8a4d2b84..22e8ecb57d 100644 --- a/tests/variantstudy/model/command/test_create_cluster.py +++ b/tests/variantstudy/model/command/test_create_cluster.py @@ -58,7 +58,7 @@ def test_init(self, command_context: CommandContext): prepro_id = command_context.matrix_service.create(prepro) modulation_id = command_context.matrix_service.create(modulation) assert cl.area_id == "foo" - assert cl.cluster_name == "cluster1" + assert cl.cluster_name == "Cluster1" assert cl.parameters.group == "nuclear" assert cl.parameters.nominal_capacity == 2400 assert cl.parameters.unit_count == 2 @@ -138,7 +138,7 @@ def test_apply(self, empty_study: FileStudy, command_context: CommandContext): clusters = configparser.ConfigParser() clusters.read(study_path / "input" / "thermal" / "clusters" / area_id / "list.ini") section = clusters[cluster_name.lower()] - assert str(section["name"]) == cluster_name.lower() + assert str(section["name"]) == cluster_name assert str(section["group"]) == parameters["group"] assert int(section["unitcount"]) == int(parameters["unitcount"]) assert float(section["nominalcapacity"]) == float(parameters["nominalcapacity"]) @@ -200,8 +200,8 @@ def test_to_dto(self, command_context: CommandContext): "action": "create_cluster", "args": { "area_id": "foo", - "cluster_name": "cluster1", - "parameters": Thermal870Properties.model_validate({"name": "cluster1", **parameters}).model_dump( + "cluster_name": "Cluster1", + "parameters": Thermal870Properties.model_validate({"name": "Cluster1", **parameters}).model_dump( mode="json", by_alias=True ), "prepro": prepro_id, diff --git a/tests/variantstudy/model/command/test_create_renewables_cluster.py b/tests/variantstudy/model/command/test_create_renewables_cluster.py index 057da0f8e4..3750dbdb03 100644 --- a/tests/variantstudy/model/command/test_create_renewables_cluster.py +++ b/tests/variantstudy/model/command/test_create_renewables_cluster.py @@ -50,9 +50,9 @@ def test_init(self, command_context: CommandContext) -> None: # Check the command data assert cl.area_id == "foo" - assert cl.cluster_name == "cluster1" + assert cl.cluster_name == "Cluster1" assert cl.parameters.model_dump(by_alias=True) == RenewableProperties.model_validate( - {"name": "cluster1", **parameters} + {"name": "Cluster1", **parameters} ).model_dump(by_alias=True) def test_validate_cluster_name(self, command_context: CommandContext) -> None: @@ -99,7 +99,7 @@ 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.lower()]["name"]) == cluster_name.lower() + assert str(clusters[cluster_name.lower()]["name"]) == cluster_name assert str(clusters[cluster_name.lower()]["ts-interpretation"]) == parameters["ts-interpretation"] output = CreateRenewablesCluster( @@ -155,8 +155,8 @@ 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": RenewableProperties.model_validate({"name": "cluster1", **parameters}).model_dump( + "cluster_name": "Cluster1", + "parameters": RenewableProperties.model_validate({"name": "Cluster1", **parameters}).model_dump( by_alias=True ), }, diff --git a/tests/variantstudy/model/command/test_create_st_storage.py b/tests/variantstudy/model/command/test_create_st_storage.py index 94f98fe4de..0f7ba1f34f 100644 --- a/tests/variantstudy/model/command/test_create_st_storage.py +++ b/tests/variantstudy/model/command/test_create_st_storage.py @@ -311,7 +311,7 @@ def test_apply__nominal_case(self, recent_study: FileStudy, command_context: Com "initiallevel": 0.5, "initialleveloptim": True, "injectionnominalcapacity": 1500, - "name": "storage1", + "name": "Storage1", "reservoircapacity": 20000, "withdrawalnominalcapacity": 1500, } @@ -349,7 +349,6 @@ 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["name"] = expected_parameters["name"].lower() expected_parameters["group"] = expected_parameters["group"].lower() # as we're using study version 8.8, we have the parameter `enabled` expected_parameters["enabled"] = True @@ -398,7 +397,7 @@ def test_match( parameters=parameters, study_version=STUDY_VERSION_8_6, ) - light_equal = area_id == cmd1.area_id and parameters["name"].lower() == cmd1.storage_name + light_equal = area_id == cmd1.area_id and parameters["name"] == cmd1.storage_name assert cmd1.match(cmd2, equal=False) == light_equal deep_equal = area_id == cmd1.area_id and parameters == PARAMETERS assert cmd1.match(cmd2, equal=True) == deep_equal @@ -433,7 +432,6 @@ def test_create_diff__not_equals(self, command_context: CommandContext): ) actual = cmd.create_diff(other) expected_params = copy.deepcopy(OTHER_PARAMETERS) - expected_params["name"] = expected_params["name"].lower() expected_params["group"] = expected_params["group"].lower() expected = [ ReplaceMatrix( diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index c1ed573009..64cb12fd09 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -300,7 +300,7 @@ args={ "area_id": "area 1", "parameters": { - "name": "storage 1", + "name": "Storage 1", "group": "battery", "injectionnominalcapacity": 0, "withdrawalnominalcapacity": 0, @@ -346,7 +346,7 @@ "initiallevel": 0, "initialleveloptim": False, "injectionnominalcapacity": 0, - "name": "storage 2", + "name": "Storage 2", "reservoircapacity": 0, "withdrawalnominalcapacity": 0, }, From 51f0ebf8b89ee901a27299c69f7a978f66976e34 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 8 Jan 2025 13:45:13 +0100 Subject: [PATCH 08/14] create new file to write unit tests --- .../filesystem/special_node/test_lower_case_nodes.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py 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..058c6b221a --- /dev/null +++ b/tests/storage/repository/filesystem/special_node/test_lower_case_nodes.py @@ -0,0 +1,11 @@ +# Copyright (c) 2024, 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 b129f0755f7e11db216756e94f238c13c9ea87ba Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 8 Jan 2025 14:44:44 +0100 Subject: [PATCH 09/14] add unit tests --- .../special_node/test_lower_case_nodes.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) 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 058c6b221a..bcd88e1974 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 @@ -9,3 +9,81 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +from pathlib import Path +from unittest.mock import Mock + +import pytest + +from antarest.study.storage.rawstudy.ini_reader import IniReader +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.mark.unit_test +@pytest.mark.parametrize( + "ini_node_cluster_class", + [InputSTStorageAreaList, ClusteredRenewableClusterConfig, InputThermalClustersAreaList], +) +def test_cluster_ini_list(tmp_path: Path, ini_node_cluster_class): + study_path = tmp_path / "study" + study_path.mkdir() + file_path = study_path / "test.ini" + file_path.touch() + data = {"Cluster 1": {"group": "Gas"}} + area_name = "area_test" + area = Area( + name=area_name, links={}, thermals=[], renewables=[], filters_synthesis=[], filters_year=[], st_storages=[] + ) + areas = {area_name: area} + node = ini_node_cluster_class( + context=Mock(), + config=FileStudyTreeConfig(study_path=study_path, path=file_path, version=-1, study_id="id", areas=areas), + area=area_name, + ) + node.save(data) + # Asserts the data is saved correctly + ini_content = IniReader().read(file_path) + assert ini_content == data + # Asserts cluster group and ids are 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"}} + # Asserts updating the file with an id not in lower case will be done correctly + node.save({"params": "43"}, ["Cluster 1"]) + content = node.get([]) + assert content == {"cluster 1": {"params": 43}} + + +@pytest.mark.unit_test +def test_binding_constraint_group_writing(tmp_path: Path): + study_path = tmp_path / "study" + study_path.mkdir() + file_path = study_path / "test.ini" + file_path.touch() + node = BindingConstraintsIni( + context=Mock(), + config=FileStudyTreeConfig(study_path=study_path, path=file_path, version=-1, study_id="id"), + ) + + data = {"0": {"name": "BC_1", "group": "GRP_1"}} + node.save(data) + # Asserts the data is saved correctly + ini_content = IniReader().read(file_path) + assert ini_content == data + # Asserts the constraint group is returned in lower case + content = node.get([]) + assert content == {"0": {"name": "BC_1", "group": "grp_1"}} From 53c213d2ad9982ac1cfd97b8ea35078dffaebf35 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Fri, 17 Jan 2025 10:39:32 +0100 Subject: [PATCH 10/14] resolve all minor comments --- .../filesystem/config/field_validators.py | 7 ------- .../model/filesystem/config/identifier.py | 21 +++---------------- .../model/filesystem/ini_file_node.py | 4 ++-- .../bindingconstraints_ini.py | 2 +- .../root/input/renewables/clusters.py | 4 ++-- .../input/st_storage/clusters/area/list.py | 2 +- .../root/input/thermal/cluster/area/list.py | 2 +- antarest/study/web/study_data_blueprint.py | 8 +++---- 8 files changed, 14 insertions(+), 36 deletions(-) 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 8c111222bb..aee910ecac 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/field_validators.py @@ -105,10 +105,3 @@ def transform_name_to_id(name: str) -> str: name: The name to convert. """ return _sub_invalid_chars(" ", name).strip().lower() - - -def validate_id_against_name(name: str) -> str: - to_return = transform_name_to_id(name) - if not to_return: - raise ValueError("Cluster name must only contains [a-zA-Z0-9],&,-,_,(,) characters") - return to_return diff --git a/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py index aaf49cb7ef..100c976255 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py +++ b/antarest/study/storage/rawstudy/model/filesystem/config/identifier.py @@ -17,6 +17,7 @@ __all__ = "LowerCaseIdentifier" from antarest.core.serialization import AntaresBaseModel +from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id class LowerCaseIdentifier( @@ -31,22 +32,6 @@ class LowerCaseIdentifier( id: str = Field(description="ID (section name)", pattern=r"[a-zA-Z0-9_(),& -]+") - @classmethod - def generate_id(cls, name: str) -> str: - """ - Generate an ID from a name. - - Args: - name: Name of a section read from an INI file - - Returns: - The ID of the section. - """ - # Avoid circular imports - from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id - - return transform_name_to_id(name) - @model_validator(mode="before") def validate_id(cls, values: t.MutableMapping[str, t.Any]) -> t.Mapping[str, t.Any]: """ @@ -64,12 +49,12 @@ def validate_id(cls, values: t.MutableMapping[str, t.Any]) -> t.Mapping[str, t.A if storage_id := values.get("id"): # If the ID is provided, it comes from a INI section name. # In some legacy case, the ID was in lower case, so we need to convert it. - values["id"] = cls.generate_id(storage_id) + values["id"] = transform_name_to_id(storage_id) return values if not values.get("name"): return values name = values["name"] - if storage_id := cls.generate_id(name): + if storage_id := transform_name_to_id(name): values["id"] = storage_id else: raise ValueError(f"Invalid name '{name}'.") diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 9c339bebdd..699ec6fc20 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -326,7 +326,7 @@ def _validate_param( raise ValueError(msg) errors.append(msg) - def get_lowered_content( + def _get_lowered_content( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False ) -> SUB_JSON: output = self._get(url, depth, expanded, get_node=False) @@ -358,7 +358,7 @@ def save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: / f"{self.config.study_id}-{self.path.relative_to(self.config.study_path).name.replace(os.sep, '.')}.lock" ) ): - info = self.get_lowered_content([]) # We read the cluster ids in lower case + info = self._get_lowered_content([]) # We read the cluster ids in lower case assert isinstance(info, dict) obj = data if isinstance(data, str): 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 30148a4bbc..1cac934d25 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 @@ -45,4 +45,4 @@ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super().get_lowered_content(url, depth, expanded) + return super()._get_lowered_content(url, depth, expanded) 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 c6ea442c10..6fc6534d92 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 @@ -33,7 +33,7 @@ def __init__( "group": str, "enabled": bool, "unitcount": int, - "nomialcapacity": float, + "nominalcapacity": float, "ts-interpretation": str, } types = {cluster_id: section for cluster_id in config.get_renewable_ids(area)} @@ -43,7 +43,7 @@ def __init__( def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super().get_lowered_content(url, depth, expanded) + return super()._get_lowered_content(url, depth, expanded) @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: 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 eea6c22c6f..99c02a375a 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 @@ -43,7 +43,7 @@ def __init__( def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super().get_lowered_content(url, depth, expanded) + return super()._get_lowered_content(url, depth, expanded) @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: 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 5bed2db83f..672956b143 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 @@ -41,7 +41,7 @@ def __init__( def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super().get_lowered_content(url, depth, expanded) + return super()._get_lowered_content(url, depth, expanded) @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index eaba4a262b..4c9801c791 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -21,7 +21,7 @@ from antarest.core.config import Config from antarest.core.jwt import JWTUser -from antarest.core.model import JSON, LowerCaseStr, StudyPermissionType +from antarest.core.model import JSON, StudyPermissionType from antarest.core.requests import RequestParameters from antarest.core.utils.utils import sanitize_uuid from antarest.core.utils.web import APITag @@ -1976,7 +1976,7 @@ def create_renewable_cluster( def update_renewable_cluster( uuid: str, area_id: str, - cluster_id: LowerCaseStr, + cluster_id: str, cluster_data: RenewableClusterInput, current_user: JWTUser = Depends(auth.get_current_user), ) -> RenewableClusterOutput: @@ -2150,7 +2150,7 @@ def create_thermal_cluster( def update_thermal_cluster( uuid: str, area_id: str, - cluster_id: LowerCaseStr, + cluster_id: str, cluster_data: ThermalClusterInput, current_user: JWTUser = Depends(auth.get_current_user), ) -> ThermalClusterOutput: @@ -2577,7 +2577,7 @@ def duplicate_cluster( uuid: str, area_id: str, cluster_type: ClusterType, - source_cluster_id: LowerCaseStr, + source_cluster_id: str, new_cluster_name: str = Query(..., alias="newName", title="New Cluster Name"), current_user: JWTUser = Depends(auth.get_current_user), ) -> t.Union[STStorageOutput, ThermalClusterOutput, RenewableClusterOutput]: From 2b323746d2adf7a7c1b78765012cd71b387c54b5 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Fri, 17 Jan 2025 10:48:20 +0100 Subject: [PATCH 11/14] fix license headers --- .../study/storage/rawstudy/model/filesystem/ini_file_node.py | 2 +- .../rawstudy/model/filesystem/root/input/renewables/clusters.py | 2 +- .../filesystem/root/input/st_storage/clusters/area/list.py | 2 +- .../model/filesystem/root/input/thermal/cluster/area/list.py | 2 +- tests/integration/study_data_blueprint/test_lower_case.py | 2 +- .../repository/filesystem/special_node/test_lower_case_nodes.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 699ec6fc20..62fe6f7b37 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -350,7 +350,7 @@ def _get_lowered_content( output = str(output).lower() return output - def save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: + def _save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: self._assert_not_in_zipped_file() with FileLock( str( 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 6fc6534d92..ac6ef27b2c 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 @@ -47,7 +47,7 @@ def get( @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - super().save_lowered_content(data, url or []) + super()._save_lowered_content(data, url or []) 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 99c02a375a..53b6eed8cb 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 @@ -47,4 +47,4 @@ def get( @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - super().save_lowered_content(data, url or []) + super()._save_lowered_content(data, url or []) 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 672956b143..98b1f23ace 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 @@ -45,4 +45,4 @@ def get( @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - super().save_lowered_content(data, url or []) + super()._save_lowered_content(data, url or []) diff --git a/tests/integration/study_data_blueprint/test_lower_case.py b/tests/integration/study_data_blueprint/test_lower_case.py index 274e042ce8..107d55a1e2 100644 --- a/tests/integration/study_data_blueprint/test_lower_case.py +++ b/tests/integration/study_data_blueprint/test_lower_case.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) +# Copyright (c) 2025, RTE (https://www.rte-france.com) # # See AUTHORS.txt # 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 bcd88e1974..9c907fe616 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 @@ -1,4 +1,4 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) +# Copyright (c) 2025, RTE (https://www.rte-france.com) # # See AUTHORS.txt # From 5ffc5a1999e05456a8c132fea2e922421dd282ff Mon Sep 17 00:00:00 2001 From: belthlemar Date: Fri, 17 Jan 2025 16:28:54 +0100 Subject: [PATCH 12/14] change save method --- .../storage/rawstudy/model/filesystem/ini_file_node.py | 6 +++--- .../model/filesystem/root/input/renewables/clusters.py | 2 +- .../filesystem/root/input/st_storage/clusters/area/list.py | 2 +- .../filesystem/root/input/thermal/cluster/area/list.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index 62fe6f7b37..e49b8c9d4d 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -350,7 +350,7 @@ def _get_lowered_content( output = str(output).lower() return output - def _save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: + def _save_content_with_lowered_keys(self, data: SUB_JSON, url: t.List[str]) -> None: self._assert_not_in_zipped_file() with FileLock( str( @@ -358,7 +358,7 @@ def _save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: / f"{self.config.study_id}-{self.path.relative_to(self.config.study_path).name.replace(os.sep, '.')}.lock" ) ): - info = self._get_lowered_content([]) # We read the cluster ids in lower case + info = self._get_lowered_content([]) # We read the INI file keys in lower case assert isinstance(info, dict) obj = data if isinstance(data, str): @@ -367,7 +367,7 @@ def _save_lowered_content(self, data: SUB_JSON, url: t.List[str]) -> None: if len(url) not in {1, 2}: info = t.cast(JSON, obj) else: - lowered_id = str(url[0]).lower() # We lower the given cluster id + lowered_id = str(url[0]).lower() # We lower the INI file keys if len(url) == 2: info.setdefault(lowered_id, {})[url[1]] = obj else: 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 ac6ef27b2c..5d72ea4729 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 @@ -47,7 +47,7 @@ def get( @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - super()._save_lowered_content(data, url or []) + super()._save_content_with_lowered_keys(data, url or []) 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 53b6eed8cb..201e4a7380 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 @@ -47,4 +47,4 @@ def get( @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - super()._save_lowered_content(data, url or []) + super()._save_content_with_lowered_keys(data, url or []) 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 98b1f23ace..ac38b03f06 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 @@ -45,4 +45,4 @@ def get( @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: - super()._save_lowered_content(data, url or []) + super()._save_content_with_lowered_keys(data, url or []) From a6a4af69672adf8b71ccc7fbd16ad3c8da68b0e9 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Fri, 17 Jan 2025 17:22:17 +0100 Subject: [PATCH 13/14] implement serializer --- .../model/filesystem/ini_file_node.py | 42 +++++++++++++------ .../bindingconstraints_ini.py | 4 +- .../root/input/renewables/clusters.py | 3 +- .../input/st_storage/clusters/area/list.py | 3 +- .../root/input/thermal/cluster/area/list.py | 3 +- 5 files changed, 39 insertions(+), 16 deletions(-) diff --git a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py index e49b8c9d4d..c0df4e7861 100644 --- a/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py +++ b/antarest/study/storage/rawstudy/model/filesystem/ini_file_node.py @@ -326,28 +326,45 @@ def _validate_param( raise ValueError(msg) errors.append(msg) - def _get_lowered_content( - self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False + def _get_content_with_specific_parsing( + self, + url: t.Optional[t.List[str]] = None, + depth: int = -1, + expanded: bool = False, + parsing_methods_for_values: t.Optional[dict[str, t.Callable[[t.Any], str]]] = None, + parsing_method_for_keys: t.Optional[t.Callable[[t.Any], str]] = None, ) -> SUB_JSON: + if not parsing_method_for_keys and not parsing_methods_for_values: # We can use the classic method + return self.get(url, depth, expanded) + output = self._get(url, depth, expanded, get_node=False) assert not isinstance(output, INode) if depth <= -1 and expanded: return output - # We need to lower the group attribute and the cluster ids if not url: assert isinstance(output, dict) for key in list(output.keys()): - new_key = str(key).lower() - output[new_key] = output.pop(key) - if "group" in output[new_key]: - output[new_key]["group"] = str(output[new_key]["group"]).lower() + new_key = key + if parsing_method_for_keys: + new_key = parsing_method_for_keys(key) + output[new_key] = output.pop(key) + if parsing_methods_for_values: + for parsed_key, method in parsing_methods_for_values.items(): + if parsed_key in output[new_key]: + output[new_key][parsed_key] = method(output[new_key][parsed_key]) + elif len(url) == 1: assert isinstance(output, dict) - if "group" in output: - output["group"] = str(output["group"]).lower() - elif len(url) == 2 and url[1] == "group": - output = str(output).lower() + if parsing_methods_for_values: + for parsed_key, method in parsing_methods_for_values.items(): + if parsed_key in output: + output[parsed_key] = method(output[parsed_key]) + + elif len(url) == 2: + if parsing_methods_for_values and url[1] in parsing_methods_for_values: + output = parsing_methods_for_values[url[1]](output) + return output def _save_content_with_lowered_keys(self, data: SUB_JSON, url: t.List[str]) -> None: @@ -358,7 +375,8 @@ def _save_content_with_lowered_keys(self, data: SUB_JSON, url: t.List[str]) -> N / f"{self.config.study_id}-{self.path.relative_to(self.config.study_path).name.replace(os.sep, '.')}.lock" ) ): - info = self._get_lowered_content([]) # We read the INI file keys in lower case + # We read the INI file keys in lower case + info = self._get_content_with_specific_parsing([], -1, False, None, lambda value: str(value).lower()) assert isinstance(info, dict) obj = data if isinstance(data, str): 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 1cac934d25..5bec90551a 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 @@ -45,4 +45,6 @@ def __init__(self, context: ContextServer, config: FileStudyTreeConfig): def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super()._get_lowered_content(url, depth, expanded) + return super()._get_content_with_specific_parsing( + url, depth, expanded, {"group": lambda value: str(value).lower()}, None + ) 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 5d72ea4729..c15410d2fe 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 @@ -43,7 +43,8 @@ def __init__( def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super()._get_lowered_content(url, depth, expanded) + func = lambda value: str(value).lower() + return super()._get_content_with_specific_parsing(url, depth, expanded, {"group": func}, func) @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: 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 201e4a7380..cd951422de 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 @@ -43,7 +43,8 @@ def __init__( def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super()._get_lowered_content(url, depth, expanded) + func = lambda value: str(value).lower() + return super()._get_content_with_specific_parsing(url, depth, expanded, {"group": func}, func) @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: 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 ac38b03f06..5e9b9719e9 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 @@ -41,7 +41,8 @@ def __init__( def get( self, url: t.Optional[t.List[str]] = None, depth: int = -1, expanded: bool = False, formatted: bool = True ) -> SUB_JSON: - return super()._get_lowered_content(url, depth, expanded) + func = lambda value: str(value).lower() + return super()._get_content_with_specific_parsing(url, depth, expanded, {"group": func}, func) @override def save(self, data: SUB_JSON, url: t.Optional[t.List[str]] = None) -> None: From f3dadab24105fdaff30edb322b9e91434264d77d Mon Sep 17 00:00:00 2001 From: belthlemar Date: Fri, 17 Jan 2025 17:46:12 +0100 Subject: [PATCH 14/14] fix broken test --- tests/variantstudy/test_command_factory.py | 78 +++++++++------------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index cb290605d6..e275ae708a 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -27,6 +27,36 @@ from antarest.study.storage.variantstudy.model.command.icommand import ICommand from antarest.study.storage.variantstudy.model.model import CommandDTO +CLUSTER_ARGS = { + "area_id": "area_name", + "cluster_name": "cluster_name", + "parameters": { + "co2": 0.0, + "enabled": True, + "fixed-cost": 0.0, + "gen-ts": "use global", + "group": "other 1", + "law.forced": "uniform", + "law.planned": "uniform", + "marginal-cost": 2.0, + "market-bid-cost": 2.0, + "min-down-time": 1, + "min-stable-power": 0.0, + "min-up-time": 1, + "must-run": False, + "name": "cluster_name", + "nominalcapacity": 2.0, + "spinning": 0.0, + "spread-cost": 0.0, + "startup-cost": 0.0, + "unitcount": 2, + "volatility.forced": 0.0, + "volatility.planned": 0.0, + }, + "prepro": "prepro", + "modulation": "modulation", +} + COMMANDS: List[CommandDTO] = [ CommandDTO(action=CommandName.CREATE_AREA.value, args={"area_name": "area_name"}, study_version=STUDY_VERSION_8_8), CommandDTO( @@ -171,54 +201,12 @@ ), 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", - }, - "prepro": "prepro", - "modulation": "modulation", - }, - study_version=STUDY_VERSION_8_8, + args=CLUSTER_ARGS, + study_version=STUDY_VERSION_8_2, ), CommandDTO( action=CommandName.CREATE_THERMAL_CLUSTER.value, - args=[ - { - "area_id": "area_name", - "cluster_name": "cluster_name", - "parameters": { - "co2": 0.0, - "enabled": True, - "fixed-cost": 0.0, - "gen-ts": "use global", - "group": "other 1", - "law.forced": "uniform", - "law.planned": "uniform", - "marginal-cost": 2.0, - "market-bid-cost": 2.0, - "min-down-time": 1, - "min-stable-power": 0.0, - "min-up-time": 1, - "must-run": False, - "name": "cluster_name", - "nominalcapacity": 2.0, - "spinning": 0.0, - "spread-cost": 0.0, - "startup-cost": 0.0, - "unitcount": 2, - "volatility.forced": 0.0, - "volatility.planned": 0.0, - }, - "prepro": "prepro", - "modulation": "modulation", - } - ], + args=[CLUSTER_ARGS], study_version=STUDY_VERSION_8_2, ), CommandDTO(