From 809a57b4e8e292f3f910e97abc551df0068e0eee Mon Sep 17 00:00:00 2001 From: MartinBelthle Date: Fri, 14 Feb 2025 14:25:29 +0100 Subject: [PATCH] refactor(areas): create user classes for Properties (#85) --- src/antares/craft/model/area.py | 89 ++++++---------- .../craft/service/api_services/factory.py | 2 +- .../craft/service/api_services/models/area.py | 53 ++++++++++ .../{area_api.py => services/area.py} | 21 ++-- src/antares/craft/service/base_services.py | 4 +- .../craft/service/local_services/factory.py | 2 +- .../service/local_services/models/area.py | 100 ++++++++++++++++++ .../{area_local.py => services/area.py} | 46 ++++---- tests/antares/delete/test_delete_api.py | 2 +- .../services/api_services/test_area_api.py | 17 +-- .../api_services/test_renewable_api.py | 2 +- .../api_services/test_st_storage_api.py | 2 +- .../services/api_services/test_study_api.py | 5 +- .../services/api_services/test_thermal_api.py | 2 +- .../services/local_services/conftest.py | 4 +- .../services/local_services/test_study.py | 72 ++++--------- tests/integration/test_web_client.py | 4 +- 17 files changed, 262 insertions(+), 165 deletions(-) create mode 100644 src/antares/craft/service/api_services/models/area.py rename src/antares/craft/service/api_services/{area_api.py => services/area.py} (95%) create mode 100644 src/antares/craft/service/local_services/models/area.py rename src/antares/craft/service/local_services/{area_local.py => services/area.py} (90%) diff --git a/src/antares/craft/model/area.py b/src/antares/craft/model/area.py index 0aff62b4..c0ef5720 100644 --- a/src/antares/craft/model/area.py +++ b/src/antares/craft/model/area.py @@ -16,12 +16,13 @@ //TO_DO to be completed as implementation progress """ +from dataclasses import dataclass, field from types import MappingProxyType -from typing import Any, Mapping, Optional +from typing import Any, Optional import pandas as pd -from antares.craft.model.commons import FilterOption, sort_filter_values +from antares.craft.model.commons import FilterOption from antares.craft.model.hydro import Hydro, HydroProperties from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties from antares.craft.model.st_storage import STStorage, STStorageProperties @@ -33,8 +34,6 @@ BaseShortTermStorageService, BaseThermalService, ) -from antares.craft.tools.alias_generators import to_space -from antares.craft.tools.all_optional_meta import all_optional_model from antares.craft.tools.contents_tool import EnumIgnoreCase, transform_name_to_id from pydantic import BaseModel, computed_field from pydantic.alias_generators import to_camel @@ -52,72 +51,44 @@ class AdequacyPatchMode(EnumIgnoreCase): VIRTUAL = "virtual" -class DefaultAreaProperties(BaseModel, extra="forbid", populate_by_name=True): - """ - DTO for updating area properties - """ +@dataclass +class AreaPropertiesUpdate: + energy_cost_unsupplied: Optional[float] = None + energy_cost_spilled: Optional[float] = None + non_dispatch_power: Optional[bool] = None + dispatch_hydro_power: Optional[bool] = None + other_dispatch_power: Optional[bool] = None + filter_synthesis: Optional[set[FilterOption]] = None + filter_by_year: Optional[set[FilterOption]] = None + adequacy_patch_mode: Optional[AdequacyPatchMode] = None + spread_unsupplied_energy_cost: Optional[float] = None + spread_spilled_energy_cost: Optional[float] = None - energy_cost_unsupplied: float = 0.0 - energy_cost_spilled: float = 0.0 - non_dispatch_power: bool = True - dispatch_hydro_power: bool = True - other_dispatch_power: bool = True - filter_synthesis: set[FilterOption] = { - FilterOption.HOURLY, - FilterOption.DAILY, - FilterOption.WEEKLY, - FilterOption.MONTHLY, - FilterOption.ANNUAL, - } - filter_by_year: set[FilterOption] = { + +def default_filtering() -> set[FilterOption]: + return { FilterOption.HOURLY, FilterOption.DAILY, FilterOption.WEEKLY, FilterOption.MONTHLY, FilterOption.ANNUAL, } - # version 830 + + +@dataclass +class AreaProperties: + energy_cost_unsupplied: float = 0.0 + energy_cost_spilled: float = 0.0 + non_dispatch_power: bool = True + dispatch_hydro_power: bool = True + other_dispatch_power: bool = True + filter_synthesis: set[FilterOption] = field(default_factory=default_filtering) + filter_by_year: set[FilterOption] = field(default_factory=default_filtering) adequacy_patch_mode: AdequacyPatchMode = AdequacyPatchMode.OUTSIDE spread_unsupplied_energy_cost: float = 0.0 spread_spilled_energy_cost: float = 0.0 -@all_optional_model -class AreaProperties(DefaultAreaProperties, alias_generator=to_camel): - pass - - -class AreaPropertiesLocal(DefaultAreaProperties, alias_generator=to_space): - @property - def nodal_optimization(self) -> Mapping[str, str]: - return { - "non-dispatchable-power": f"{self.non_dispatch_power}".lower(), - "dispatchable-hydro-power": f"{self.dispatch_hydro_power}".lower(), - "other-dispatchable-power": f"{self.other_dispatch_power}".lower(), - "spread-unsupplied-energy-cost": f"{self.spread_unsupplied_energy_cost:.6f}", - "spread-spilled-energy-cost": f"{self.spread_spilled_energy_cost:.6f}", - } - - @property - def filtering(self) -> Mapping[str, str]: - return { - "filter-synthesis": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_synthesis)), - "filter-year-by-year": ", ".join(filter_value for filter_value in sort_filter_values(self.filter_by_year)), - } - - def adequacy_patch(self) -> dict[str, dict[str, str]]: - return {"adequacy-patch": {"adequacy-patch-mode": self.adequacy_patch_mode.value}} - - def yield_local_dict(self) -> dict[str, Mapping[str, str]]: - args = {"nodal optimization": self.nodal_optimization} - args.update({"filtering": self.filtering}) - return args - - def yield_area_properties(self) -> AreaProperties: - excludes = {"filtering", "nodal_optimization"} - return AreaProperties.model_validate(self.model_dump(mode="json", exclude=excludes)) - - class AreaUi(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): """ DTO for updating area UI @@ -323,7 +294,7 @@ def delete_st_storages(self, storages: list[STStorage]) -> None: def delete_st_storage(self, storage: STStorage) -> None: self.delete_st_storages([storage]) - def update_properties(self, properties: AreaProperties) -> None: + def update_properties(self, properties: AreaPropertiesUpdate) -> None: new_properties = self._area_service.update_area_properties(self.id, properties) self._properties = new_properties diff --git a/src/antares/craft/service/api_services/factory.py b/src/antares/craft/service/api_services/factory.py index dcf19fd8..ff104575 100644 --- a/src/antares/craft/service/api_services/factory.py +++ b/src/antares/craft/service/api_services/factory.py @@ -18,8 +18,8 @@ from antares.craft.api_conf.request_wrapper import RequestWrapper from antares.craft.exceptions.exceptions import APIError, StudyCreationError, StudyImportError, StudyMoveError from antares.craft.model.study import Study -from antares.craft.service.api_services.area_api import AreaApiService from antares.craft.service.api_services.link_api import LinkApiService +from antares.craft.service.api_services.services.area import AreaApiService from antares.craft.service.api_services.services.binding_constraint import BindingConstraintApiService from antares.craft.service.api_services.services.hydro import HydroApiService from antares.craft.service.api_services.services.output import OutputApiService diff --git a/src/antares/craft/service/api_services/models/area.py b/src/antares/craft/service/api_services/models/area.py new file mode 100644 index 00000000..b92a506a --- /dev/null +++ b/src/antares/craft/service/api_services/models/area.py @@ -0,0 +1,53 @@ +# 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 dataclasses import asdict +from typing import Union + +from antares.craft.model.area import AdequacyPatchMode, AreaProperties, AreaPropertiesUpdate +from antares.craft.model.commons import FilterOption +from antares.craft.service.api_services.models.base_model import APIBaseModel +from antares.craft.tools.all_optional_meta import all_optional_model + +AreaPropertiesType = Union[AreaProperties, AreaPropertiesUpdate] + + +@all_optional_model +class AreaPropertiesAPI(APIBaseModel): + energy_cost_unsupplied: float + energy_cost_spilled: float + non_dispatch_power: bool + dispatch_hydro_power: bool + other_dispatch_power: bool + filter_synthesis: set[FilterOption] + filter_by_year: set[FilterOption] + adequacy_patch_mode: AdequacyPatchMode + spread_unsupplied_energy_cost: float + spread_spilled_energy_cost: float + + @staticmethod + def from_user_model(user_class: AreaPropertiesType) -> "AreaPropertiesAPI": + user_dict = asdict(user_class) + return AreaPropertiesAPI.model_validate(user_dict) + + def to_user_model(self) -> AreaProperties: + return AreaProperties( + energy_cost_unsupplied=self.energy_cost_spilled, + energy_cost_spilled=self.energy_cost_spilled, + non_dispatch_power=self.non_dispatch_power, + dispatch_hydro_power=self.dispatch_hydro_power, + other_dispatch_power=self.other_dispatch_power, + filter_synthesis=self.filter_synthesis, + filter_by_year=self.filter_by_year, + adequacy_patch_mode=self.adequacy_patch_mode, + spread_unsupplied_energy_cost=self.spread_unsupplied_energy_cost, + spread_spilled_energy_cost=self.spread_spilled_energy_cost, + ) diff --git a/src/antares/craft/service/api_services/area_api.py b/src/antares/craft/service/api_services/services/area.py similarity index 95% rename from src/antares/craft/service/api_services/area_api.py rename to src/antares/craft/service/api_services/services/area.py index c1b3d526..46a2100c 100644 --- a/src/antares/craft/service/api_services/area_api.py +++ b/src/antares/craft/service/api_services/services/area.py @@ -32,11 +32,12 @@ ThermalCreationError, ThermalDeletionError, ) -from antares.craft.model.area import Area, AreaProperties, AreaUi +from antares.craft.model.area import Area, AreaProperties, AreaPropertiesUpdate, AreaUi from antares.craft.model.hydro import Hydro from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties from antares.craft.model.st_storage import STStorage, STStorageProperties from antares.craft.model.thermal import ThermalCluster, ThermalClusterProperties +from antares.craft.service.api_services.models.area import AreaPropertiesAPI from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI from antares.craft.service.api_services.models.st_storage import STStoragePropertiesAPI from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI @@ -99,7 +100,10 @@ def create_area( if properties: url = f"{base_area_url}/{area_id}/properties/form" - body = properties.model_dump(mode="json", exclude_none=True) + api_model = AreaPropertiesAPI.from_user_model(properties) + # todo: change this exclude when AntaresWeb will work + exclude = {"spread_unsupplied_energy_cost", "spread_spilled_energy_cost"} + body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude=exclude) if body: self._wrapper.put(url, json=body) if ui: @@ -122,7 +126,8 @@ def create_area( url = f"{base_area_url}/{area_id}/properties/form" response = self._wrapper.get(url) - area_properties = AreaProperties.model_validate(response.json()) + api_properties = AreaPropertiesAPI.model_validate(response.json()) + area_properties = api_properties.to_user_model() # TODO: Ask AntaresWeb to do the same endpoint for only one area url = f"{base_area_url}?type=AREA&ui=true" @@ -354,14 +359,18 @@ def create_misc_gen(self, area_id: str, series: pd.DataFrame) -> None: raise MatrixUploadError(area_id, "misc-gen", e.message) from e @override - def update_area_properties(self, area_id: str, properties: AreaProperties) -> AreaProperties: + def update_area_properties(self, area_id: str, properties: AreaPropertiesUpdate) -> AreaProperties: url = f"{self._base_url}/studies/{self.study_id}/areas/{area_id}/properties/form" try: - body = properties.model_dump(mode="json", exclude_none=True) + api_model = AreaPropertiesAPI.from_user_model(properties) + # todo: change this exclude when AntaresWeb will work + exclude = {"spread_unsupplied_energy_cost", "spread_spilled_energy_cost"} + body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True, exclude=exclude) self._wrapper.put(url, json=body) response = self._wrapper.get(url) - area_properties = AreaProperties.model_validate(response.json()) + api_properties = AreaPropertiesAPI.model_validate(response.json()) + area_properties = api_properties.to_user_model() except APIError as e: raise AreaPropertiesUpdateError(area_id, e.message) from e diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index c55b60e5..d46b87b0 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -22,7 +22,7 @@ from antares.craft.model.simulation import AntaresSimulationParameters, Job if TYPE_CHECKING: - from antares.craft.model.area import Area, AreaProperties, AreaUi + from antares.craft.model.area import Area, AreaProperties, AreaPropertiesUpdate, AreaUi from antares.craft.model.binding_constraint import ( BindingConstraint, BindingConstraintProperties, @@ -179,7 +179,7 @@ def create_misc_gen(self, area_id: str, series: pd.DataFrame) -> None: pass @abstractmethod - def update_area_properties(self, area_id: str, properties: "AreaProperties") -> "AreaProperties": + def update_area_properties(self, area_id: str, properties: "AreaPropertiesUpdate") -> "AreaProperties": """ Args: area_id: concerned area diff --git a/src/antares/craft/service/local_services/factory.py b/src/antares/craft/service/local_services/factory.py index 56bd2bbd..92df0801 100644 --- a/src/antares/craft/service/local_services/factory.py +++ b/src/antares/craft/service/local_services/factory.py @@ -21,8 +21,8 @@ from antares.craft.service.base_services import ( StudyServices, ) -from antares.craft.service.local_services.area_local import AreaLocalService from antares.craft.service.local_services.link_local import LinkLocalService +from antares.craft.service.local_services.services.area import AreaLocalService from antares.craft.service.local_services.services.binding_constraint import BindingConstraintLocalService from antares.craft.service.local_services.services.hydro import HydroLocalService from antares.craft.service.local_services.services.output import OutputLocalService diff --git a/src/antares/craft/service/local_services/models/area.py b/src/antares/craft/service/local_services/models/area.py new file mode 100644 index 00000000..a6669b85 --- /dev/null +++ b/src/antares/craft/service/local_services/models/area.py @@ -0,0 +1,100 @@ +# 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 typing import Any, Union + +from antares.craft.model.area import AdequacyPatchMode, AreaProperties, AreaPropertiesUpdate, default_filtering +from antares.craft.model.commons import FilterOption +from antares.craft.service.local_services.models.base_model import LocalBaseModel +from antares.craft.tools.alias_generators import to_kebab +from pydantic import Field, field_validator + +AreaPropertiesType = Union[AreaProperties, AreaPropertiesUpdate] + + +class OptimizationPropertiesLocal(LocalBaseModel, alias_generator=to_kebab): + non_dispatchable_power: bool = True + dispatchable_hydro_power: bool = True + other_dispatchable_power: bool = True + spread_unsupplied_energy_cost: float = 0.0 + spread_spilled_energy_cost: float = 0.0 + + +class FilteringPropertiesLocal(LocalBaseModel, alias_generator=to_kebab): + filter_synthesis: set[FilterOption] = Field(default_factory=default_filtering) + filter_year_by_year: set[FilterOption] = Field(default_factory=default_filtering) + + @field_validator("filter_synthesis", "filter_year_by_year", mode="before") + def validate_accuracy_on_correlation(cls, v: Any) -> set[str]: + if isinstance(v, (list, set)): + return set(v) + if isinstance(v, str): + if v[0] == "[": + v = v[1:-1] + return set(v.replace(" ", "").split(",")) + raise ValueError(f"Value {v} not supported for filtering") + + +class AdequacyPatchPropertiesLocal(LocalBaseModel, alias_generator=to_kebab): + adequacy_patch_mode: AdequacyPatchMode = AdequacyPatchMode.OUTSIDE + + +class AreaPropertiesLocal(LocalBaseModel): + nodal_optimization: OptimizationPropertiesLocal = Field(alias="nodal optimization") + filtering: FilteringPropertiesLocal + adequacy_patch: AdequacyPatchPropertiesLocal = Field(alias="adequacy-patch") + energy_cost_unsupplied: float = 0.0 + energy_cost_spilled: float = 0.0 + + @staticmethod + def from_user_model(user_class: AreaPropertiesType) -> "AreaPropertiesLocal": + args = { + "adequacy_patch": {"adequacy_patch_mode": user_class.adequacy_patch_mode}, + "filtering": { + "filter_synthesis": user_class.filter_synthesis, + "filter_year_by_year": user_class.filter_by_year, + }, + "nodal_optimization": { + "non_dispatchable_power": user_class.non_dispatch_power, + "dispatchable_hydro_power": user_class.dispatch_hydro_power, + "other_dispatchable_power": user_class.other_dispatch_power, + "spread_unsupplied_energy_cost": user_class.spread_unsupplied_energy_cost, + "spread_spilled_energy_cost": user_class.spread_spilled_energy_cost, + }, + "energy_cost_unsupplied": user_class.energy_cost_unsupplied, + "energy_cost_spilled": user_class.energy_cost_spilled, + } + + return AreaPropertiesLocal.model_validate(args) + + def to_user_model(self) -> AreaProperties: + return AreaProperties( + energy_cost_unsupplied=self.energy_cost_unsupplied, + energy_cost_spilled=self.energy_cost_spilled, + non_dispatch_power=self.nodal_optimization.non_dispatchable_power, + dispatch_hydro_power=self.nodal_optimization.dispatchable_hydro_power, + other_dispatch_power=self.nodal_optimization.other_dispatchable_power, + filter_synthesis=self.filtering.filter_synthesis, + filter_by_year=self.filtering.filter_year_by_year, + adequacy_patch_mode=self.adequacy_patch.adequacy_patch_mode, + spread_unsupplied_energy_cost=self.nodal_optimization.spread_unsupplied_energy_cost, + spread_spilled_energy_cost=self.nodal_optimization.spread_spilled_energy_cost, + ) + + def to_adequacy_ini(self) -> dict[str, dict[str, str]]: + return self.model_dump(mode="json", include={"adequacy_patch"}, by_alias=True) + + def to_optimization_ini(self) -> dict[str, dict[str, str]]: + args = self.model_dump(mode="json", include={"nodal_optimization", "filtering"}, by_alias=True) + args["filtering"]["filter-synthesis"] = ", ".join(sorted(args["filtering"]["filter-synthesis"])) + args["filtering"]["filter-year-by-year"] = ", ".join(sorted(args["filtering"]["filter-year-by-year"])) + return args diff --git a/src/antares/craft/service/local_services/area_local.py b/src/antares/craft/service/local_services/services/area.py similarity index 90% rename from src/antares/craft/service/local_services/area_local.py rename to src/antares/craft/service/local_services/services/area.py index 6e73ef98..85f0804c 100644 --- a/src/antares/craft/service/local_services/area_local.py +++ b/src/antares/craft/service/local_services/services/area.py @@ -23,7 +23,7 @@ from antares.craft.model.area import ( Area, AreaProperties, - AreaPropertiesLocal, + AreaPropertiesUpdate, AreaUi, AreaUiLocal, ) @@ -38,6 +38,7 @@ BaseShortTermStorageService, BaseThermalService, ) +from antares.craft.service.local_services.models.area import AreaPropertiesLocal from antares.craft.service.local_services.models.renewable import RenewableClusterPropertiesLocal from antares.craft.service.local_services.models.st_storage import STStoragePropertiesLocal from antares.craft.service.local_services.models.thermal import ThermalClusterPropertiesLocal @@ -262,20 +263,17 @@ def _line_exists_in_file(file_content: str, line_to_add: str) -> bool: with (self.config.study_path / InitializationFilesTypes.AREAS_SETS_INI.value).open("w") as sets_ini: sets_ini_content.write(sets_ini) - local_properties = ( - AreaPropertiesLocal.model_validate(properties.model_dump(mode="json", exclude_none=True)) - if properties - else AreaPropertiesLocal() - ) + properties = properties or AreaProperties() + local_properties = AreaPropertiesLocal.from_user_model(properties) adequacy_patch_ini = IniFile( self.config.study_path, InitializationFilesTypes.AREA_ADEQUACY_PATCH_INI, area_name ) - adequacy_patch_ini.add_section(local_properties.adequacy_patch()) + adequacy_patch_ini.add_section(local_properties.to_adequacy_ini()) adequacy_patch_ini.write_ini_file() optimization_ini = ConfigParser() - optimization_ini.read_dict(local_properties.yield_local_dict()) + optimization_ini.read_dict(local_properties.to_optimization_ini()) with open(new_area_directory / "optimization.ini", "w") as optimization_ini_file: optimization_ini.write(optimization_ini_file) @@ -325,7 +323,7 @@ def _line_exists_in_file(file_content: str, line_to_add: str) -> bool: renewable_service=self.renewable_service, hydro_service=self.hydro_service, hydro=hydro, - properties=local_properties.yield_area_properties(), + properties=local_properties.to_user_model(), # round-trip to do the validation inside Pydantic ui=local_ui.yield_area_ui(), ) return created_area @@ -335,7 +333,7 @@ def delete_area(self, area_id: str) -> None: raise NotImplementedError @override - def update_area_properties(self, area_id: str, properties: AreaProperties) -> AreaProperties: + def update_area_properties(self, area_id: str, properties: AreaPropertiesUpdate) -> AreaProperties: raise NotImplementedError @override @@ -387,23 +385,21 @@ def read_areas(self) -> List[Area]: area_adequacy_dict = IniFile( self.config.study_path, InitializationFilesTypes.AREA_ADEQUACY_PATCH_INI, area_id=element.name ).ini_dict + thermal_area_dict = IniFile(self.config.study_path, InitializationFilesTypes.THERMAL_AREAS_INI).ini_dict + unserverd_energy_cost = thermal_area_dict.get("unserverdenergycost", {}).get(element.name, 0) + spilled_energy_cost = thermal_area_dict.get("spilledenergycost", {}).get(element.name, 0) + local_properties_dict = { + **optimization_dict, + **area_adequacy_dict, + "energy_cost_unsupplied": unserverd_energy_cost, + "energy_cost_spilled": spilled_energy_cost, + } + local_properties = AreaPropertiesLocal.model_validate(local_properties_dict) + area_properties = local_properties.to_user_model() ui_dict = IniFile( self.config.study_path, InitializationFilesTypes.AREA_UI_INI, area_id=element.name ).ini_dict - thermal_area_dict = IniFile(self.config.study_path, InitializationFilesTypes.THERMAL_AREAS_INI).ini_dict - nodal_optimization = optimization_dict["nodal optimization"] - area_properties = AreaPropertiesLocal( - non_dispatch_power=nodal_optimization.get("non-dispatchable-power"), - dispatch_hydro_power=nodal_optimization.get("dispatchable-hydro-power"), - other_dispatch_power=nodal_optimization.get("other-dispatchable-power"), - spread_unsupplied_energy_cost=nodal_optimization.get("spread-unsupplied-energy-cost"), - spread_spilled_energy_cost=nodal_optimization.get("spread-spilled-energy-cost"), - energy_cost_unsupplied=thermal_area_dict["unserverdenergycost"].get(element.name), - energy_cost_spilled=thermal_area_dict["spilledenergycost"].get(element.name), - filter_synthesis=set(optimization_dict["filtering"].get("filter-synthesis").split(", ")), - filter_by_year=set(optimization_dict["filtering"].get("filter-year-by-year").split(", ")), - adequacy_patch_mode=area_adequacy_dict["adequacy-patch"].get("adequacy-patch-mode"), - ) + ui_properties = AreaUi( layer=ui_dict["ui"].get("layer"), x=ui_dict["ui"].get("x"), @@ -424,7 +420,7 @@ def read_areas(self) -> List[Area]: thermal_service=self.thermal_service, renewable_service=self.renewable_service, hydro_service=self.hydro_service, - properties=area_properties.yield_area_properties(), + properties=area_properties, ui=ui_properties, ) area.hydro.read_properties() diff --git a/tests/antares/delete/test_delete_api.py b/tests/antares/delete/test_delete_api.py index 2c377762..e234be05 100644 --- a/tests/antares/delete/test_delete_api.py +++ b/tests/antares/delete/test_delete_api.py @@ -29,8 +29,8 @@ from antares.craft.model.renewable import RenewableCluster from antares.craft.model.st_storage import STStorage from antares.craft.model.thermal import ThermalCluster -from antares.craft.service.api_services.area_api import AreaApiService from antares.craft.service.api_services.link_api import LinkApiService +from antares.craft.service.api_services.services.area import AreaApiService from antares.craft.service.api_services.services.binding_constraint import BindingConstraintApiService from antares.craft.service.api_services.services.hydro import HydroApiService from antares.craft.service.api_services.services.output import OutputApiService diff --git a/tests/antares/services/api_services/test_area_api.py b/tests/antares/services/api_services/test_area_api.py index 983871c6..92beb81c 100644 --- a/tests/antares/services/api_services/test_area_api.py +++ b/tests/antares/services/api_services/test_area_api.py @@ -24,18 +24,19 @@ STStorageCreationError, ThermalCreationError, ) -from antares.craft.model.area import Area, AreaProperties, AreaUi +from antares.craft.model.area import Area, AreaPropertiesUpdate, AreaUi from antares.craft.model.hydro import Hydro, HydroProperties, HydroPropertiesUpdate from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties from antares.craft.model.st_storage import STStorage from antares.craft.model.study import Study from antares.craft.model.thermal import ThermalCluster, ThermalClusterProperties -from antares.craft.service.api_services.area_api import AreaApiService from antares.craft.service.api_services.factory import create_api_services +from antares.craft.service.api_services.models.area import AreaPropertiesAPI from antares.craft.service.api_services.models.hydro import HydroPropertiesAPI from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI from antares.craft.service.api_services.models.st_storage import STStoragePropertiesAPI from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI +from antares.craft.service.api_services.services.area import AreaApiService class TestCreateAPI: @@ -64,17 +65,17 @@ class TestCreateAPI: def test_update_area_properties_success(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area.id}/properties/form" - properties = AreaProperties() - properties.energy_cost_spilled = 1 + properties = AreaPropertiesUpdate(energy_cost_spilled=1) + api_properties = AreaPropertiesAPI.from_user_model(properties) mocker.put(url, status_code=200) - mocker.get(url, json=properties.model_dump(mode="json"), status_code=200) + mocker.get(url, json=api_properties.model_dump(mode="json", by_alias=True), status_code=200) self.area.update_properties(properties=properties) + assert self.area.properties.energy_cost_spilled == 1 def test_update_area_properties_fails(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/areas/{self.area.id}/properties/form" - properties = AreaProperties() - properties.energy_cost_unsupplied = 100 + properties = AreaPropertiesUpdate(energy_cost_unsupplied=100) antares_web_description_msg = "Server KO" mocker.put(url, json={"description": antares_web_description_msg}, status_code=404) with pytest.raises( @@ -324,7 +325,7 @@ def test_read_areas_success(self): renewables={renewable_id: renewable_cluster}, st_storages={storage_id: st_storage}, hydro=hydro, - properties=json_properties, + properties=AreaPropertiesAPI.model_validate(json_properties).to_user_model(), ui=area_ui, ) diff --git a/tests/antares/services/api_services/test_renewable_api.py b/tests/antares/services/api_services/test_renewable_api.py index db781ed1..7505a717 100644 --- a/tests/antares/services/api_services/test_renewable_api.py +++ b/tests/antares/services/api_services/test_renewable_api.py @@ -24,9 +24,9 @@ ) from antares.craft.model.area import Area from antares.craft.model.renewable import RenewableCluster, RenewableClusterProperties, RenewableClusterPropertiesUpdate -from antares.craft.service.api_services.area_api import AreaApiService from antares.craft.service.api_services.factory import create_api_services from antares.craft.service.api_services.models.renewable import RenewableClusterPropertiesAPI +from antares.craft.service.api_services.services.area import AreaApiService from antares.craft.service.api_services.services.renewable import RenewableApiService diff --git a/tests/antares/services/api_services/test_st_storage_api.py b/tests/antares/services/api_services/test_st_storage_api.py index f7bf2ff6..75e882ee 100644 --- a/tests/antares/services/api_services/test_st_storage_api.py +++ b/tests/antares/services/api_services/test_st_storage_api.py @@ -24,9 +24,9 @@ ) from antares.craft.model.area import Area from antares.craft.model.st_storage import STStorage, STStorageProperties -from antares.craft.service.api_services.area_api import AreaApiService from antares.craft.service.api_services.factory import create_api_services from antares.craft.service.api_services.models.st_storage import STStoragePropertiesAPI +from antares.craft.service.api_services.services.area import AreaApiService from antares.craft.service.api_services.services.st_storage import ShortTermStorageApiService diff --git a/tests/antares/services/api_services/test_study_api.py b/tests/antares/services/api_services/test_study_api.py index d3cf770d..1bd14b09 100644 --- a/tests/antares/services/api_services/test_study_api.py +++ b/tests/antares/services/api_services/test_study_api.py @@ -39,7 +39,7 @@ StudyVariantCreationError, ThermalTimeseriesGenerationError, ) -from antares.craft.model.area import Area, AreaProperties, AreaUi +from antares.craft.model.area import Area, AreaUi from antares.craft.model.binding_constraint import ( BindingConstraintFrequency, BindingConstraintOperator, @@ -54,6 +54,7 @@ from antares.craft.model.simulation import AntaresSimulationParameters, Job, JobStatus, Solver from antares.craft.model.study import Study from antares.craft.service.api_services.factory import create_api_services +from antares.craft.service.api_services.models.area import AreaPropertiesAPI from antares.craft.service.api_services.models.binding_constraint import BindingConstraintPropertiesAPI from antares.craft.service.api_services.models.hydro import HydroPropertiesAPI from antares.craft.service.api_services.services.output import OutputApiService @@ -153,7 +154,7 @@ def test_create_area_success(self): url2 = f"{base_url}/studies/{self.study_id}/areas/{area_name}/properties/form" url3 = f"{base_url}/studies/{self.study_id}/areas/{area_name}/hydro/form" mocker.put(url2, status_code=201) - mocker.get(url2, json=AreaProperties().model_dump(), status_code=200) + mocker.get(url2, json=AreaPropertiesAPI().model_dump(), status_code=200) mocker.get(url3, json=HydroPropertiesAPI().model_dump()) area = self.study.create_area(area_name) assert isinstance(area, Area) diff --git a/tests/antares/services/api_services/test_thermal_api.py b/tests/antares/services/api_services/test_thermal_api.py index aa9bca2d..1006eacf 100644 --- a/tests/antares/services/api_services/test_thermal_api.py +++ b/tests/antares/services/api_services/test_thermal_api.py @@ -30,9 +30,9 @@ ThermalClusterProperties, ThermalClusterPropertiesUpdate, ) -from antares.craft.service.api_services.area_api import AreaApiService from antares.craft.service.api_services.factory import create_api_services from antares.craft.service.api_services.models.thermal import ThermalClusterPropertiesAPI +from antares.craft.service.api_services.services.area import AreaApiService from antares.craft.service.api_services.services.thermal import ThermalApiService diff --git a/tests/antares/services/local_services/conftest.py b/tests/antares/services/local_services/conftest.py index 0a7962c7..536a5336 100644 --- a/tests/antares/services/local_services/conftest.py +++ b/tests/antares/services/local_services/conftest.py @@ -14,7 +14,7 @@ import pandas as pd from antares.craft import create_study_local -from antares.craft.model.area import Area, AreaPropertiesLocal +from antares.craft.model.area import Area, AreaProperties from antares.craft.model.binding_constraint import ( BindingConstraint, BindingConstraintFrequency, @@ -46,7 +46,7 @@ def local_study(tmp_path) -> Study: def local_study_w_areas(tmp_path, local_study) -> Study: areas_to_create = ["fr", "it"] for area in areas_to_create: - area_properties = AreaPropertiesLocal(energy_cost_spilled="1.000000", energy_cost_unsupplied="0.500000") + area_properties = AreaProperties(energy_cost_spilled=1, energy_cost_unsupplied=0.5) local_study.create_area(area, properties=area_properties) return local_study diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 9e286b71..c09bbb45 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -30,7 +30,7 @@ CustomError, LinkCreationError, ) -from antares.craft.model.area import AreaProperties, AreaPropertiesLocal, AreaUi, AreaUiLocal +from antares.craft.model.area import AreaProperties, AreaUi, AreaUiLocal from antares.craft.model.binding_constraint import ( BindingConstraint, BindingConstraintFrequency, @@ -517,15 +517,15 @@ def test_area_optimization_ini_content(self, tmp_path, local_study): expected_optimization_ini_path = study_antares_path / "input" / "areas" / "area1" / "optimization.ini" expected_optimization_ini_content = """[nodal optimization] -non-dispatchable-power = true -dispatchable-hydro-power = true -other-dispatchable-power = true -spread-unsupplied-energy-cost = 0.000000 -spread-spilled-energy-cost = 0.000000 +non-dispatchable-power = True +dispatchable-hydro-power = True +other-dispatchable-power = True +spread-unsupplied-energy-cost = 0.0 +spread-spilled-energy-cost = 0.0 [filtering] -filter-synthesis = hourly, daily, weekly, monthly, annual -filter-year-by-year = hourly, daily, weekly, monthly, annual +filter-synthesis = annual, daily, hourly, monthly, weekly +filter-year-by-year = annual, daily, hourly, monthly, weekly """ @@ -560,15 +560,15 @@ def test_custom_area_optimization_ini_content(self, tmp_path, local_study): tmp_path / local_study.name / "input/areas" / area_to_create / "optimization.ini" ) expected_optimization_ini_content = """[nodal optimization] -non-dispatchable-power = true -dispatchable-hydro-power = false -other-dispatchable-power = true -spread-unsupplied-energy-cost = 0.000000 -spread-spilled-energy-cost = 0.000000 +non-dispatchable-power = True +dispatchable-hydro-power = False +other-dispatchable-power = True +spread-unsupplied-energy-cost = 0.0 +spread-spilled-energy-cost = 0.0 [filtering] -filter-synthesis = hourly, daily, weekly, monthly, annual -filter-year-by-year = hourly, weekly, annual +filter-synthesis = annual, daily, hourly, monthly, weekly +filter-year-by-year = annual, hourly, weekly """ @@ -626,7 +626,9 @@ def mock_error_in_sets_ini(): area_id = "test" - monkeypatch.setattr("antares.craft.service.local_services.area_local._sets_ini_content", mock_error_in_sets_ini) + monkeypatch.setattr( + "antares.craft.service.local_services.services.area._sets_ini_content", mock_error_in_sets_ini + ) with pytest.raises( AreaCreationError, match=f"Could not create the area {area_id}: {error_message}", @@ -690,27 +692,8 @@ def test_creating_duplicate_area_name_errors(self, local_study_w_areas): local_study_w_areas.create_area(area_to_create) def test_areas_have_default_properties(self, tmp_path, local_study_w_areas): - # Given - expected_default_properties = { - "nodal optimization": { - "non-dispatchable-power": "true", - "dispatchable-hydro-power": "true", - "other-dispatchable-power": "true", - "spread-unsupplied-energy-cost": "0.000000", - "spread-spilled-energy-cost": "0.000000", - }, - "filtering": { - "filter-synthesis": "hourly, daily, weekly, monthly, annual", - "filter-year-by-year": "hourly, daily, weekly, monthly, annual", - }, - } - - # When actual_area_properties = local_study_w_areas.get_areas()["fr"].properties - created_properties = actual_area_properties.model_dump(mode="json", exclude_none=True) - actual_properties = AreaPropertiesLocal.model_validate(created_properties).yield_local_dict() - - assert expected_default_properties == actual_properties + assert actual_area_properties == AreaProperties(energy_cost_spilled=1, energy_cost_unsupplied=0.5) def test_areas_with_custom_properties(self, tmp_path, local_study): # Given @@ -721,25 +704,10 @@ def test_areas_with_custom_properties(self, tmp_path, local_study): energy_cost_spilled=3.5, filter_by_year={FilterOption.ANNUAL, FilterOption.ANNUAL, FilterOption.HOURLY, FilterOption.WEEKLY}, ) - expected_properties = { - "nodal optimization": { - "non-dispatchable-power": "true", - "dispatchable-hydro-power": "false", - "other-dispatchable-power": "true", - "spread-unsupplied-energy-cost": "1.000000", - "spread-spilled-energy-cost": "0.000000", - }, - "filtering": { - "filter-synthesis": "hourly, daily, weekly, monthly, annual", - "filter-year-by-year": "hourly, weekly, annual", - }, - } # When created_area = local_study.create_area(area_name=area_to_create, properties=area_properties) - created_properties = created_area.properties.model_dump(mode="json", exclude_none=True) - actual_properties = AreaPropertiesLocal.model_validate(created_properties).yield_local_dict() - assert expected_properties == actual_properties + assert created_area.properties == area_properties def test_areas_ini_has_correct_sections(self, actual_thermal_areas_ini): # Given diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index 52d560de..751acc00 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -124,9 +124,7 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert area_be.ui.color_rgb == area_ui.color_rgb # tests area creation with properties - properties = AreaProperties() - properties.energy_cost_spilled = 100 - properties.adequacy_patch_mode = AdequacyPatchMode.INSIDE + properties = AreaProperties(energy_cost_spilled=100, adequacy_patch_mode=AdequacyPatchMode.INSIDE) properties.filter_synthesis = [FilterOption.HOURLY, FilterOption.DAILY, FilterOption.HOURLY] area_name = "DE" area_de = study.create_area(area_name, properties=properties)