From 45d00e946590dc50b1d5d73186f14a4ba5e41ace Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 16:55:43 +0100 Subject: [PATCH 01/27] start work --- src/antares/craft/model/binding_constraint.py | 34 +++++------- .../api_services/models/binding_constraint.py | 52 +++++++++++++++++++ .../binding_constraint.py} | 16 ++++-- src/antares/craft/service/base_services.py | 3 +- .../models/binding_constraint.py | 52 +++++++++++++++++++ .../binding_constraint.py} | 46 ++-------------- src/antares/craft/service/service_factory.py | 4 +- 7 files changed, 135 insertions(+), 72 deletions(-) create mode 100644 src/antares/craft/service/api_services/models/binding_constraint.py rename src/antares/craft/service/api_services/{binding_constraint_api.py => services/binding_constraint.py} (90%) create mode 100644 src/antares/craft/service/local_services/models/binding_constraint.py rename src/antares/craft/service/local_services/{binding_constraint_local.py => services/binding_constraint.py} (83%) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index f87375ac..a90eb8f9 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -9,17 +9,15 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from dataclasses import dataclass from enum import Enum from typing import Any, Optional, Union import pandas as pd from antares.craft.service.base_services import BaseBindingConstraintService -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, Field, model_validator -from pydantic.alias_generators import to_camel class BindingConstraintFrequency(EnumIgnoreCase): @@ -92,20 +90,19 @@ def generate_id(cls, data: Union[dict[str, str], LinkData, ClusterData]) -> str: return ".".join((data.area.lower(), data.cluster.lower())) -class DefaultBindingConstraintProperties(BaseModel, extra="forbid", populate_by_name=True, alias_generator=to_camel): - """Default properties for binding constraints - - Attributes: - enabled (bool): True - time_step (BindingConstraintFrequency): BindingConstraintFrequency.HOURLY - operator (BindingConstraintOperator): BindingConstraintOperator.LESS - comments (str): None - filter_year_by_year (str): "hourly" - filter_synthesis (str): "hourly" - group (str): "default" +@dataclass +class BindingConstraintPropertiesUpdate: + enabled: Optional[bool] = None + time_step: Optional[BindingConstraintFrequency] = None + operator: Optional[BindingConstraintOperator] = None + comments: Optional[str] = None + filter_year_by_year: Optional[str] = None + filter_synthesis: Optional[str] = None + group: Optional[str] = None - """ +@dataclass +class BindingConstraintProperties: enabled: bool = True time_step: BindingConstraintFrequency = BindingConstraintFrequency.HOURLY operator: BindingConstraintOperator = BindingConstraintOperator.LESS @@ -115,11 +112,6 @@ class DefaultBindingConstraintProperties(BaseModel, extra="forbid", populate_by_ group: str = "default" -@all_optional_model -class BindingConstraintProperties(DefaultBindingConstraintProperties): - pass - - class BindingConstraint: def __init__( self, @@ -162,7 +154,7 @@ def delete_term(self, term: ConstraintTerm) -> None: self._binding_constraint_service.delete_binding_constraint_term(self.id, term.id) self._terms.pop(term.id) - def update_properties(self, properties: BindingConstraintProperties) -> None: + def update_properties(self, properties: BindingConstraintPropertiesUpdate) -> None: new_properties = self._binding_constraint_service.update_binding_constraint_properties(self, properties) self._properties = new_properties diff --git a/src/antares/craft/service/api_services/models/binding_constraint.py b/src/antares/craft/service/api_services/models/binding_constraint.py new file mode 100644 index 00000000..924f6e3d --- /dev/null +++ b/src/antares/craft/service/api_services/models/binding_constraint.py @@ -0,0 +1,52 @@ +# 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.binding_constraint import ( + BindingConstraintFrequency, + BindingConstraintOperator, + BindingConstraintProperties, + BindingConstraintPropertiesUpdate, +) +from antares.craft.service.api_services.models import APIBaseModel +from antares.craft.tools.all_optional_meta import all_optional_model + +BindingConstraintPropertiesType = Union[BindingConstraintProperties, BindingConstraintPropertiesUpdate] + + +@all_optional_model +class BindingConstraintPropertiesAPI(APIBaseModel): + enabled: bool + time_step: BindingConstraintFrequency + operator: BindingConstraintOperator + comments: str + filter_year_by_year: str + filter_synthesis: str + group: str + + @staticmethod + def from_user_model(user_class: BindingConstraintPropertiesType) -> "BindingConstraintPropertiesAPI": + user_dict = asdict(user_class) + return BindingConstraintPropertiesAPI.model_validate(user_dict) + + def to_user_model(self) -> BindingConstraintProperties: + return BindingConstraintProperties( + enabled=self.enabled, + time_step=self.time_step, + operator=self.operator, + comments=self.comments, + filter_year_by_year=self.filter_year_by_year, + filter_synthesis=self.filter_synthesis, + group=self.group, + ) diff --git a/src/antares/craft/service/api_services/binding_constraint_api.py b/src/antares/craft/service/api_services/services/binding_constraint.py similarity index 90% rename from src/antares/craft/service/api_services/binding_constraint_api.py rename to src/antares/craft/service/api_services/services/binding_constraint.py index a0fd83bc..fc7d3bfa 100644 --- a/src/antares/craft/service/api_services/binding_constraint_api.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -30,9 +30,11 @@ from antares.craft.model.binding_constraint import ( BindingConstraint, BindingConstraintProperties, + BindingConstraintPropertiesUpdate, ConstraintMatrixName, ConstraintTerm, ) +from antares.craft.service.api_services.models.binding_constraint import BindingConstraintPropertiesAPI from antares.craft.service.api_services.utils import get_matrix from antares.craft.service.base_services import BaseBindingConstraintService from typing_extensions import override @@ -77,7 +79,8 @@ def create_binding_constraint( try: body = {"name": name} if properties: - camel_properties = properties.model_dump(mode="json", by_alias=True, exclude_none=True) + api_model = BindingConstraintPropertiesAPI.from_user_model(properties) + camel_properties = api_model.model_dump(mode="json", by_alias=True, exclude_none=True) body = {**body, **camel_properties} for matrix, matrix_name in zip( [less_term_matrix, equal_term_matrix, greater_term_matrix], @@ -90,7 +93,8 @@ def create_binding_constraint( bc_id = created_properties["id"] for key in ["terms", "id", "name"]: del created_properties[key] - bc_properties = BindingConstraintProperties.model_validate(created_properties) + api_properties = BindingConstraintPropertiesAPI.model_validate(created_properties) + bc_properties = api_properties.to_user_model() bc_terms: list[ConstraintTerm] = [] if terms: @@ -120,11 +124,12 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No @override def update_binding_constraint_properties( - self, binding_constraint: BindingConstraint, properties: BindingConstraintProperties + self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate ) -> BindingConstraintProperties: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{binding_constraint.id}" try: - body = properties.model_dump(mode="json", by_alias=True, exclude_none=True) + api_model = BindingConstraintPropertiesAPI.from_user_model(properties) + body = api_model.model_dump(mode="json", by_alias=True, exclude_none=True) if not body: return binding_constraint.properties @@ -132,7 +137,8 @@ def update_binding_constraint_properties( json_response = response.json() for key in ["terms", "id", "name"]: del json_response[key] - new_properties = BindingConstraintProperties.model_validate(json_response) + new_api_properties = BindingConstraintPropertiesAPI.model_validate(json_response) + new_properties = new_api_properties.to_user_model() except APIError as e: raise ConstraintPropertiesUpdateError(binding_constraint.id, e.message) from e diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index 5ae6c668..3ff31475 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -25,6 +25,7 @@ from antares.craft.model.binding_constraint import ( BindingConstraint, BindingConstraintProperties, + BindingConstraintPropertiesUpdate, ConstraintMatrixName, ConstraintTerm, ) @@ -491,7 +492,7 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No @abstractmethod def update_binding_constraint_properties( - self, binding_constraint: "BindingConstraint", properties: "BindingConstraintProperties" + self, binding_constraint: "BindingConstraint", properties: "BindingConstraintPropertiesUpdate" ) -> "BindingConstraintProperties": """ Args: diff --git a/src/antares/craft/service/local_services/models/binding_constraint.py b/src/antares/craft/service/local_services/models/binding_constraint.py new file mode 100644 index 00000000..812dcf91 --- /dev/null +++ b/src/antares/craft/service/local_services/models/binding_constraint.py @@ -0,0 +1,52 @@ +# 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.binding_constraint import ( + BindingConstraintFrequency, + BindingConstraintOperator, + BindingConstraintProperties, + BindingConstraintPropertiesUpdate, +) +from antares.craft.service.local_services.models import LocalBaseModel +from antares.craft.tools.all_optional_meta import all_optional_model + +BindingConstraintPropertiesType = Union[BindingConstraintProperties, BindingConstraintPropertiesUpdate] + + +@all_optional_model +class BindingConstraintPropertiesLocal(LocalBaseModel): + enabled: bool + time_step: BindingConstraintFrequency + operator: BindingConstraintOperator + comments: str + filter_year_by_year: str + filter_synthesis: str + group: str + + @staticmethod + def from_user_model(user_class: BindingConstraintPropertiesType) -> "BindingConstraintPropertiesLocal": + user_dict = asdict(user_class) + return BindingConstraintPropertiesLocal.model_validate(user_dict) + + def to_user_model(self) -> BindingConstraintProperties: + return BindingConstraintProperties( + enabled=self.enabled, + time_step=self.time_step, + operator=self.operator, + comments=self.comments, + filter_year_by_year=self.filter_year_by_year, + filter_synthesis=self.filter_synthesis, + group=self.group, + ) diff --git a/src/antares/craft/service/local_services/binding_constraint_local.py b/src/antares/craft/service/local_services/services/binding_constraint.py similarity index 83% rename from src/antares/craft/service/local_services/binding_constraint_local.py rename to src/antares/craft/service/local_services/services/binding_constraint.py index fc73c965..80887611 100644 --- a/src/antares/craft/service/local_services/binding_constraint_local.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -21,58 +21,18 @@ BindingConstraintFrequency, BindingConstraintOperator, BindingConstraintProperties, + BindingConstraintPropertiesUpdate, ConstraintMatrixName, ConstraintTerm, - DefaultBindingConstraintProperties, ) from antares.craft.service.base_services import BaseBindingConstraintService +from antares.craft.service.local_services.models.binding_constraint import BindingConstraintPropertiesLocal from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes from antares.craft.tools.matrix_tool import df_read, df_save from antares.craft.tools.time_series_tool import TimeSeriesFileType -from pydantic import Field from typing_extensions import override -class BindingConstraintPropertiesLocal(DefaultBindingConstraintProperties): - """ - Used to create the entries for the bindingconstraints.ini file - - Attributes: - constraint_name: The constraint name - constraint_id: The constraint id - properties (BindingConstraintProperties): The BindingConstraintProperties to set - terms (dict[str, ConstraintTerm]]): The terms applying to the binding constraint - """ - - constraint_name: str - constraint_id: str - terms: dict[str, ConstraintTerm] = Field(default_factory=dict[str, ConstraintTerm]) - - @property - def list_ini_fields(self) -> dict[str, str]: - ini_dict = { - "name": self.constraint_name, - "id": self.constraint_id, - "enabled": f"{self.enabled}".lower(), - "type": self.time_step.value, - "operator": self.operator.value, - "comments": self.comments, - "filter-year-by-year": self.filter_year_by_year, - "filter-synthesis": self.filter_synthesis, - "group": self.group, - } | {term_id: term.weight_offset() for term_id, term in self.terms.items()} - return {key: value for key, value in ini_dict.items() if value not in [None, ""]} - - def yield_binding_constraint_properties(self) -> BindingConstraintProperties: - excludes = { - "constraint_name", - "constraint_id", - "terms", - "list_ini_fields", - } - return BindingConstraintProperties(**self.model_dump(mode="json", exclude=excludes)) - - class BindingConstraintLocalService(BaseBindingConstraintService): def __init__(self, config: LocalConfiguration, study_name: str, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -232,7 +192,7 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No @override def update_binding_constraint_properties( - self, binding_constraint: BindingConstraint, properties: BindingConstraintProperties + self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate ) -> BindingConstraintProperties: raise NotImplementedError diff --git a/src/antares/craft/service/service_factory.py b/src/antares/craft/service/service_factory.py index 2083095d..8f9aa277 100644 --- a/src/antares/craft/service/service_factory.py +++ b/src/antares/craft/service/service_factory.py @@ -14,9 +14,9 @@ from antares.craft.config.base_configuration import BaseConfiguration from antares.craft.config.local_configuration import LocalConfiguration from antares.craft.service.api_services.area_api import AreaApiService -from antares.craft.service.api_services.binding_constraint_api import BindingConstraintApiService from antares.craft.service.api_services.hydro_api import HydroApiService from antares.craft.service.api_services.link_api import LinkApiService +from antares.craft.service.api_services.services.binding_constraint import BindingConstraintApiService from antares.craft.service.api_services.services.output import OutputApiService from antares.craft.service.api_services.services.renewable import RenewableApiService from antares.craft.service.api_services.services.run import RunApiService @@ -38,9 +38,9 @@ BaseThermalService, ) from antares.craft.service.local_services.area_local import AreaLocalService -from antares.craft.service.local_services.binding_constraint_local import BindingConstraintLocalService from antares.craft.service.local_services.hydro_local import HydroLocalService from antares.craft.service.local_services.link_local import LinkLocalService +from antares.craft.service.local_services.services.binding_constraint import BindingConstraintLocalService from antares.craft.service.local_services.services.output import OutputLocalService from antares.craft.service.local_services.services.renewable import RenewableLocalService from antares.craft.service.local_services.services.run import RunLocalService From 8481c0ebd1550863b89e249243fe93b46953f6c6 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 16:57:11 +0100 Subject: [PATCH 02/27] continue --- src/antares/craft/model/binding_constraint.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index a90eb8f9..5c65f75b 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -138,10 +138,6 @@ def id(self) -> str: def properties(self) -> BindingConstraintProperties: return self._properties - @properties.setter - def properties(self, new_properties: BindingConstraintProperties) -> None: - self._properties = new_properties - def get_terms(self) -> dict[str, ConstraintTerm]: return self._terms From c5dad01b235f0a9f47dbd2ae240254d12e3d489a Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:05:14 +0100 Subject: [PATCH 03/27] fix imports issue --- tests/antares/delete/test_delete_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/antares/delete/test_delete_api.py b/tests/antares/delete/test_delete_api.py index 51c23357..1fcecd09 100644 --- a/tests/antares/delete/test_delete_api.py +++ b/tests/antares/delete/test_delete_api.py @@ -30,8 +30,8 @@ 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.binding_constraint_api import BindingConstraintApiService from antares.craft.service.api_services.link_api import LinkApiService +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 from antares.craft.service.api_services.services.renewable import RenewableApiService From 1ca5c270a20af109a5167363e147069b28f4817d Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:09:59 +0100 Subject: [PATCH 04/27] add test --- .../test_binding_constraint_api.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/antares/services/api_services/test_binding_constraint_api.py b/tests/antares/services/api_services/test_binding_constraint_api.py index c61c4613..3d46019f 100644 --- a/tests/antares/services/api_services/test_binding_constraint_api.py +++ b/tests/antares/services/api_services/test_binding_constraint_api.py @@ -18,8 +18,14 @@ from antares.craft.api_conf.api_conf import APIconf from antares.craft.exceptions.exceptions import ConstraintMatrixDownloadError, ConstraintPropertiesUpdateError from antares.craft.model.area import Area -from antares.craft.model.binding_constraint import BindingConstraint, BindingConstraintProperties, ConstraintMatrixName +from antares.craft.model.binding_constraint import ( + BindingConstraint, + BindingConstraintProperties, + BindingConstraintPropertiesUpdate, + ConstraintMatrixName, +) from antares.craft.model.study import Study +from antares.craft.service.api_services.models.binding_constraint import BindingConstraintPropertiesAPI from antares.craft.service.service_factory import ServiceFactory @@ -50,13 +56,17 @@ class TestCreateAPI: def test_update_binding_constraint_properties_success(self): with requests_mock.Mocker() as mocker: - properties = BindingConstraintProperties(enabled=False) + update_properties = BindingConstraintPropertiesUpdate(enabled=False) + api_properties = BindingConstraintPropertiesAPI.from_user_model(update_properties) constraint = BindingConstraint( "bc_1", ServiceFactory(self.api, self.study_id).create_binding_constraints_service() ) url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint.id}" - mocker.put(url, json={"id": "id", "name": "name", "terms": [], **properties.model_dump()}, status_code=200) - constraint.update_properties(properties=properties) + mocker.put( + url, json={"id": "id", "name": "name", "terms": [], **api_properties.model_dump()}, status_code=200 + ) + constraint.update_properties(properties=update_properties) + assert constraint.properties == BindingConstraintProperties(enabled=False) def test_update_binding_constraint_properties_fails(self): with requests_mock.Mocker() as mocker: From 6458c9d8571377d2803d301b37f8dbd081410200 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:13:18 +0100 Subject: [PATCH 05/27] add test --- .../service/api_services/services/binding_constraint.py | 4 ++-- tests/antares/services/api_services/test_study_api.py | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index fc7d3bfa..07e82589 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -196,9 +196,9 @@ def read_binding_constraints(self) -> list[BindingConstraint]: BindingConstraint( constraint["name"], self, - BindingConstraintProperties.model_validate( + BindingConstraintPropertiesAPI.model_validate( {k: v for k, v in constraint.items() if k not in ["terms", "id", "name"]} - ), + ).to_user_model(), [ConstraintTerm.model_validate(term) for term in constraint["terms"]], ) for constraint in constraints_json diff --git a/tests/antares/services/api_services/test_study_api.py b/tests/antares/services/api_services/test_study_api.py index d745b5a6..b37b154a 100644 --- a/tests/antares/services/api_services/test_study_api.py +++ b/tests/antares/services/api_services/test_study_api.py @@ -53,6 +53,7 @@ from antares.craft.model.settings.study_settings import StudySettingsUpdate from antares.craft.model.simulation import AntaresSimulationParameters, Job, JobStatus, Solver from antares.craft.model.study import Study, create_study_api, create_variant_api, import_study_api, read_study_api +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 from antares.craft.service.service_factory import ServiceFactory @@ -199,11 +200,13 @@ def test_create_link_success(self): def test_create_binding_constraint_success(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints" - json_response = BindingConstraintProperties().model_dump(mode="json", by_alias=True) + properties = BindingConstraintProperties(enabled=False, filter_synthesis="annual") + json_response = BindingConstraintPropertiesAPI.from_user_model(properties).model_dump(mode="json", by_alias=True) constraint_name = "bc_1" mocker.post(url, json={"id": "id", "name": constraint_name, "terms": [], **json_response}, status_code=201) - constraint = self.study.create_binding_constraint(name=constraint_name) - assert isinstance(constraint, BindingConstraint) + constraint = self.study.create_binding_constraint(name=constraint_name, properties=properties) + assert constraint.name == constraint_name + assert constraint.properties == properties def test_create_binding_constraint_fails(self): with requests_mock.Mocker() as mocker: From 6efc1072afcd03c8f989d79d89346cc3d8af4498 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:16:41 +0100 Subject: [PATCH 06/27] fix test --- .../services/api_services/test_binding_constraint_api.py | 7 +++++-- tests/antares/services/api_services/test_study_api.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/antares/services/api_services/test_binding_constraint_api.py b/tests/antares/services/api_services/test_binding_constraint_api.py index 3d46019f..c46887d7 100644 --- a/tests/antares/services/api_services/test_binding_constraint_api.py +++ b/tests/antares/services/api_services/test_binding_constraint_api.py @@ -57,13 +57,16 @@ class TestCreateAPI: def test_update_binding_constraint_properties_success(self): with requests_mock.Mocker() as mocker: update_properties = BindingConstraintPropertiesUpdate(enabled=False) - api_properties = BindingConstraintPropertiesAPI.from_user_model(update_properties) + creation_properties = BindingConstraintProperties(enabled=False) + api_properties = BindingConstraintPropertiesAPI.from_user_model(creation_properties) constraint = BindingConstraint( "bc_1", ServiceFactory(self.api, self.study_id).create_binding_constraints_service() ) url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint.id}" mocker.put( - url, json={"id": "id", "name": "name", "terms": [], **api_properties.model_dump()}, status_code=200 + url, + json={"id": "id", "name": "name", "terms": [], **api_properties.model_dump(mode="json")}, + status_code=200, ) constraint.update_properties(properties=update_properties) assert constraint.properties == BindingConstraintProperties(enabled=False) diff --git a/tests/antares/services/api_services/test_study_api.py b/tests/antares/services/api_services/test_study_api.py index b37b154a..dbae4938 100644 --- a/tests/antares/services/api_services/test_study_api.py +++ b/tests/antares/services/api_services/test_study_api.py @@ -40,7 +40,6 @@ ) from antares.craft.model.area import Area, AreaProperties, AreaUi from antares.craft.model.binding_constraint import ( - BindingConstraint, BindingConstraintFrequency, BindingConstraintOperator, BindingConstraintProperties, @@ -201,7 +200,9 @@ def test_create_binding_constraint_success(self): with requests_mock.Mocker() as mocker: url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints" properties = BindingConstraintProperties(enabled=False, filter_synthesis="annual") - json_response = BindingConstraintPropertiesAPI.from_user_model(properties).model_dump(mode="json", by_alias=True) + json_response = BindingConstraintPropertiesAPI.from_user_model(properties).model_dump( + mode="json", by_alias=True + ) constraint_name = "bc_1" mocker.post(url, json={"id": "id", "name": constraint_name, "terms": [], **json_response}, status_code=201) constraint = self.study.create_binding_constraint(name=constraint_name, properties=properties) From 84e5d4044f361f2c022b7c8ffb424a1de99185f3 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:36:23 +0100 Subject: [PATCH 07/27] continue --- .../services/binding_constraint.py | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index 80887611..83cd1fe8 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -27,6 +27,7 @@ ) from antares.craft.service.base_services import BaseBindingConstraintService from antares.craft.service.local_services.models.binding_constraint import BindingConstraintPropertiesLocal +from antares.craft.tools.contents_tool import transform_name_to_id from antares.craft.tools.ini_tool import IniFile, InitializationFilesTypes from antares.craft.tools.matrix_tool import df_read, df_save from antares.craft.tools.time_series_tool import TimeSeriesFileType @@ -50,6 +51,7 @@ def create_binding_constraint( equal_term_matrix: Optional[pd.DataFrame] = None, greater_term_matrix: Optional[pd.DataFrame] = None, ) -> BindingConstraint: + properties = properties or BindingConstraintProperties() constraint = BindingConstraint( name=name, binding_constraint_service=self, @@ -57,8 +59,7 @@ def create_binding_constraint( terms=terms, ) - local_properties = self._generate_local_properties(constraint) - constraint.properties = local_properties.yield_binding_constraint_properties() + local_properties = BindingConstraintPropertiesLocal.from_user_model(properties) current_ini_content = self.ini_file.ini_dict_binding_constraints or {} if any(values.get("id") == constraint.id for values in current_ini_content.values()): @@ -66,7 +67,7 @@ def create_binding_constraint( constraint_name=name, message=f"A binding constraint with the name {name} already exists." ) - self._write_binding_constraint_ini(local_properties, name, name, terms) + self._create_constraint_inside_ini(name, local_properties, terms) self._store_time_series(constraint, less_term_matrix, equal_term_matrix, greater_term_matrix) @@ -114,11 +115,37 @@ def _check_if_empty_ts(time_step: BindingConstraintFrequency, time_series: Optio time_series_length = (365 * 24 + 24) if time_step == BindingConstraintFrequency.HOURLY else 366 return time_series if time_series is not None else pd.DataFrame(np.zeros([time_series_length, 1])) + def _create_constraint_inside_ini( + self, + constraint_name: str, + properties: BindingConstraintPropertiesLocal, + terms: Optional[list[ConstraintTerm]] = None, + ) -> None: + current_ini_content = self.ini_file.ini_dict + constraint_id = transform_name_to_id(constraint_name) + # Ensures the constraint doesn't already exist + for existing_constraint in current_ini_content.values(): + if existing_constraint["id"] == constraint_id: + raise BindingConstraintCreationError( + constraint_name=constraint_name, + message=f"A binding constraint with the name {constraint_name} already exists.", + ) + new_key = str(len(current_ini_content.keys())) + props_content = { + "id": constraint_id, + "name": constraint_name, + **properties.model_dump(mode="json", by_alias=True), + } + term_content = {} if not terms else {term.id: term.weight_offset() for term in terms} + whole_content = props_content | term_content + current_ini_content[new_key] = whole_content + self.ini_file.ini_dict = current_ini_content + self.ini_file.write_ini_file() + def _write_binding_constraint_ini( self, local_properties: BindingConstraintPropertiesLocal, constraint_name: str, - constraint_id: str, terms: Optional[list[ConstraintTerm]] = None, ) -> None: """ @@ -180,7 +207,6 @@ def add_constraint_terms(self, constraint: BindingConstraint, terms: list[Constr self._write_binding_constraint_ini( local_properties=local_properties, constraint_name=constraint.name, - constraint_id=constraint.id, terms=terms_values, ) From 903bf23ac0493ab62812ecef45381661b65ad3e1 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:36:57 +0100 Subject: [PATCH 08/27] continue --- .../service/local_services/services/binding_constraint.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index 83cd1fe8..3f809bed 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -61,12 +61,6 @@ def create_binding_constraint( local_properties = BindingConstraintPropertiesLocal.from_user_model(properties) - current_ini_content = self.ini_file.ini_dict_binding_constraints or {} - if any(values.get("id") == constraint.id for values in current_ini_content.values()): - raise BindingConstraintCreationError( - constraint_name=name, message=f"A binding constraint with the name {name} already exists." - ) - self._create_constraint_inside_ini(name, local_properties, terms) self._store_time_series(constraint, less_term_matrix, equal_term_matrix, greater_term_matrix) From d1aa21f18a2c0baa7d2c5e539bef52db551f6d72 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:45:00 +0100 Subject: [PATCH 09/27] continue --- src/antares/craft/exceptions/exceptions.py | 6 +++++ .../services/binding_constraint.py | 22 ++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/antares/craft/exceptions/exceptions.py b/src/antares/craft/exceptions/exceptions.py index 26de1af1..501a58fc 100644 --- a/src/antares/craft/exceptions/exceptions.py +++ b/src/antares/craft/exceptions/exceptions.py @@ -205,6 +205,12 @@ def __init__(self, constraint_name: str, message: str) -> None: super().__init__(self.message) +class ConstraintDoesNotExistError(Exception): + def __init__(self, constraint_name: str) -> None: + self.message = f"The binding constraint {constraint_name} doesn't exist: " + super().__init__(self.message) + + class ConstraintMatrixUpdateError(Exception): def __init__(self, constraint_name: str, matrix_name: str, message: str) -> None: self.message = f"Could not update matrix {matrix_name} for binding constraint {constraint_name}: " + message diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index 3f809bed..9eeb8a39 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -15,7 +15,10 @@ import pandas as pd from antares.craft.config.local_configuration import LocalConfiguration -from antares.craft.exceptions.exceptions import BindingConstraintCreationError +from antares.craft.exceptions.exceptions import ( + BindingConstraintCreationError, + ConstraintDoesNotExistError, +) from antares.craft.model.binding_constraint import ( BindingConstraint, BindingConstraintFrequency, @@ -136,6 +139,23 @@ def _create_constraint_inside_ini( self.ini_file.ini_dict = current_ini_content self.ini_file.write_ini_file() + def _update_constraint_inside_ini( + self, + constraint_name: str, + properties: Optional[BindingConstraintPropertiesLocal] = None, + terms: Optional[list[ConstraintTerm]] = None, + ) -> None: + current_ini_content = self.ini_file.ini_dict + constraint_id = transform_name_to_id(constraint_name) + # Ensures the constraint already exists + existing_key = next((key for key, bc in current_ini_content.items() if bc["id"] == constraint_id), None) + if not existing_key: + raise ConstraintDoesNotExistError(constraint_name) + + # todo: do the update + # existing_constraint = current_ini_content[existing_key] + # todo: split the content between terms and properties + def _write_binding_constraint_ini( self, local_properties: BindingConstraintPropertiesLocal, From f2a6b815affcaab08dccf4f78bfc46027af79548 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:47:10 +0100 Subject: [PATCH 10/27] add aliases --- .../local_services/models/binding_constraint.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/antares/craft/service/local_services/models/binding_constraint.py b/src/antares/craft/service/local_services/models/binding_constraint.py index d415deb7..f115ac8a 100644 --- a/src/antares/craft/service/local_services/models/binding_constraint.py +++ b/src/antares/craft/service/local_services/models/binding_constraint.py @@ -20,20 +20,19 @@ BindingConstraintPropertiesUpdate, ) from antares.craft.service.local_services.models.base_model import LocalBaseModel -from antares.craft.tools.all_optional_meta import all_optional_model +from pydantic import Field BindingConstraintPropertiesType = Union[BindingConstraintProperties, BindingConstraintPropertiesUpdate] -@all_optional_model class BindingConstraintPropertiesLocal(LocalBaseModel): - enabled: bool - time_step: BindingConstraintFrequency - operator: BindingConstraintOperator - comments: str - filter_year_by_year: str - filter_synthesis: str - group: str + enabled: bool = True + time_step: BindingConstraintFrequency = Field(BindingConstraintFrequency.HOURLY, alias="type") + operator: BindingConstraintOperator = BindingConstraintOperator.LESS + comments: str = "" + filter_year_by_year: str = Field("hourly", alias="filter-year-by-year") + filter_synthesis: str = Field("hourly", alias="filter-synthesis") + group: str = "default" @staticmethod def from_user_model(user_class: BindingConstraintPropertiesType) -> "BindingConstraintPropertiesLocal": From dde0328e42bfcaf52c4d364b3f0acb3f724a7808 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:49:43 +0100 Subject: [PATCH 11/27] add aliases --- .../service/local_services/services/binding_constraint.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index 9eeb8a39..d6fc0783 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -142,8 +142,7 @@ def _create_constraint_inside_ini( def _update_constraint_inside_ini( self, constraint_name: str, - properties: Optional[BindingConstraintPropertiesLocal] = None, - terms: Optional[list[ConstraintTerm]] = None, + properties: BindingConstraintPropertiesLocal, ) -> None: current_ini_content = self.ini_file.ini_dict constraint_id = transform_name_to_id(constraint_name) From cef297ce26367663372e2a97036de4ca71eb676a Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:56:10 +0100 Subject: [PATCH 12/27] implement method --- .../services/binding_constraint.py | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index d6fc0783..b93a5caa 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -139,22 +139,6 @@ def _create_constraint_inside_ini( self.ini_file.ini_dict = current_ini_content self.ini_file.write_ini_file() - def _update_constraint_inside_ini( - self, - constraint_name: str, - properties: BindingConstraintPropertiesLocal, - ) -> None: - current_ini_content = self.ini_file.ini_dict - constraint_id = transform_name_to_id(constraint_name) - # Ensures the constraint already exists - existing_key = next((key for key, bc in current_ini_content.items() if bc["id"] == constraint_id), None) - if not existing_key: - raise ConstraintDoesNotExistError(constraint_name) - - # todo: do the update - # existing_constraint = current_ini_content[existing_key] - # todo: split the content between terms and properties - def _write_binding_constraint_ini( self, local_properties: BindingConstraintPropertiesLocal, @@ -233,7 +217,18 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No def update_binding_constraint_properties( self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate ) -> BindingConstraintProperties: - raise NotImplementedError + current_ini_content = self.ini_file.ini_dict + # Ensures the constraint already exists + existing_key = next((key for key, bc in current_ini_content.items() if bc["id"] == binding_constraint.id), None) + if not existing_key: + raise ConstraintDoesNotExistError(binding_constraint.name) + + local_properties = BindingConstraintPropertiesLocal.from_user_model(properties) + existing_constraint = current_ini_content[existing_key] + existing_constraint.update(local_properties.model_dump(mode="json", by_alias=True)) + self.ini_file.ini_dict = current_ini_content + self.ini_file.write_ini_file() + return local_properties.to_user_model() @override def get_constraint_matrix(self, constraint: BindingConstraint, matrix_name: ConstraintMatrixName) -> pd.DataFrame: From 8ecc1c60939645cd0d38da31864223aba2611f41 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 17:56:38 +0100 Subject: [PATCH 13/27] implement method --- .../service/local_services/services/binding_constraint.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index b93a5caa..72620479 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -64,7 +64,7 @@ def create_binding_constraint( local_properties = BindingConstraintPropertiesLocal.from_user_model(properties) - self._create_constraint_inside_ini(name, local_properties, terms) + self._create_constraint_inside_ini(name, local_properties, terms or []) self._store_time_series(constraint, less_term_matrix, equal_term_matrix, greater_term_matrix) @@ -116,7 +116,7 @@ def _create_constraint_inside_ini( self, constraint_name: str, properties: BindingConstraintPropertiesLocal, - terms: Optional[list[ConstraintTerm]] = None, + terms: list[ConstraintTerm], ) -> None: current_ini_content = self.ini_file.ini_dict constraint_id = transform_name_to_id(constraint_name) @@ -133,7 +133,7 @@ def _create_constraint_inside_ini( "name": constraint_name, **properties.model_dump(mode="json", by_alias=True), } - term_content = {} if not terms else {term.id: term.weight_offset() for term in terms} + term_content = {term.id: term.weight_offset() for term in terms} whole_content = props_content | term_content current_ini_content[new_key] = whole_content self.ini_file.ini_dict = current_ini_content From b01706e02eec77866874d19e69a38ba4da56b148 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:04:55 +0100 Subject: [PATCH 14/27] fix mypy issues --- src/antares/craft/model/binding_constraint.py | 4 +- .../services/binding_constraint.py | 8 +- src/antares/craft/service/base_services.py | 4 +- .../services/binding_constraint.py | 82 ++++--------------- 4 files changed, 19 insertions(+), 79 deletions(-) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index 5c65f75b..b416286f 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -142,8 +142,8 @@ def get_terms(self) -> dict[str, ConstraintTerm]: return self._terms def add_terms(self, terms: list[ConstraintTerm]) -> None: - added_terms = self._binding_constraint_service.add_constraint_terms(self, terms) - for term in added_terms: + self._binding_constraint_service.add_constraint_terms(self, terms) + for term in terms: self._terms[term.id] = term def delete_term(self, term: ConstraintTerm) -> None: diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index 07e82589..8e71c4ba 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -170,21 +170,15 @@ def update_constraint_matrix( raise ConstraintMatrixUpdateError(constraint.id, matrix_name.value, e.message) from e @override - def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: + def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint.id}" try: json_terms = [term.model_dump() for term in terms] self._wrapper.post(f"{url}/terms", json=json_terms) - response = self._wrapper.get(url) - all_terms = response.json()["terms"] - validated_terms = [ConstraintTerm.model_validate(term) for term in all_terms] - new_terms = [term for term in validated_terms if term.id not in constraint.get_terms()] except APIError as e: raise ConstraintTermAdditionError(constraint.id, [term.id for term in terms], e.message) from e - return new_terms - @override def read_binding_constraints(self) -> list[BindingConstraint]: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints" diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index b91e24df..78b1fc44 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -516,9 +516,7 @@ def create_binding_constraint( pass @abstractmethod - def add_constraint_terms( - self, constraint: "BindingConstraint", terms: list["ConstraintTerm"] - ) -> list["ConstraintTerm"]: + def add_constraint_terms(self, constraint: "BindingConstraint", terms: list["ConstraintTerm"]) -> None: """ Args: constraint: the concerned binding constraint diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index 72620479..e0795de0 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from typing import Any, Optional, Union +from typing import Any, Optional import numpy as np import pandas as pd @@ -70,18 +70,6 @@ def create_binding_constraint( return constraint - @staticmethod - def _create_local_property_args(constraint: BindingConstraint) -> dict[str, Union[str, dict[str, ConstraintTerm]]]: - return { - "constraint_name": constraint.name, - "constraint_id": constraint.id, - "terms": constraint.get_terms(), - **constraint.properties.model_dump(mode="json", exclude_none=True), - } - - def _generate_local_properties(self, constraint: BindingConstraint) -> BindingConstraintPropertiesLocal: - return BindingConstraintPropertiesLocal.model_validate(self._create_local_property_args(constraint)) - def _store_time_series( self, constraint: BindingConstraint, @@ -139,75 +127,35 @@ def _create_constraint_inside_ini( self.ini_file.ini_dict = current_ini_content self.ini_file.write_ini_file() - def _write_binding_constraint_ini( - self, - local_properties: BindingConstraintPropertiesLocal, - constraint_name: str, - terms: Optional[list[ConstraintTerm]] = None, - ) -> None: - """ - Write or update a binding constraint in the INI file. - - """ - - current_ini_content = self.ini_file.ini_dict_binding_constraints or {} - - existing_section = next( - (section for section, values in current_ini_content.items() if values.get("name") == constraint_name), - None, - ) - - if existing_section: - existing_terms = current_ini_content[existing_section] - - serialized_terms = {term.id: term.weight_offset() for term in terms} if terms else {} - - existing_terms.update(serialized_terms) - current_ini_content[existing_section] = existing_terms - - # Persist the updated INI content - self.ini_file.write_ini_file() - else: - section_index = len(current_ini_content) - current_ini_content[str(section_index)] = local_properties.list_ini_fields - - self.ini_file.ini_dict_binding_constraints = current_ini_content - self.ini_file.write_ini_file() - @override - def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> list[ConstraintTerm]: + def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> None: """ Add terms to a binding constraint and update the INI file. Args: constraint (BindingConstraint): The binding constraint to update. terms (list[ConstraintTerm]): A list of new terms to add. - - Returns: - list[ConstraintTerm]: The updated list of terms. """ - new_terms = constraint.get_terms().copy() - + # Checks the terms to add are not already defined + current_terms = constraint.get_terms() for term in terms: - if term.id in constraint.get_terms(): + if term.id in current_terms: raise BindingConstraintCreationError( constraint_name=constraint.name, message=f"Duplicate term found: {term.id}" ) - new_terms[term.id] = term - local_properties = self._generate_local_properties(constraint) - local_properties.terms = new_terms - - terms_values = list(new_terms.values()) - - self._write_binding_constraint_ini( - local_properties=local_properties, - constraint_name=constraint.name, - terms=terms_values, - ) + current_ini_content = self.ini_file.ini_dict_binding_constraints or {} + # Ensures the constraint already exists + existing_key = next((key for key, bc in current_ini_content.items() if bc["id"] == constraint.id), None) + if not existing_key: + raise ConstraintDoesNotExistError(constraint.name) - return terms_values + existing_constraint = current_ini_content[existing_key] + new_terms = {term.id: term.weight_offset() for term in terms} + new_content = existing_constraint | new_terms + self.ini_file.ini_dict = new_content + self.ini_file.write_ini_file() @override def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: From 5a7f21c4ddcf6dd95016582f456983c505bd6e39 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:08:47 +0100 Subject: [PATCH 15/27] fix mypy issues --- .../services/binding_constraint.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index e0795de0..bc5092f4 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -146,12 +146,7 @@ def add_constraint_terms(self, constraint: BindingConstraint, terms: list[Constr ) current_ini_content = self.ini_file.ini_dict_binding_constraints or {} - # Ensures the constraint already exists - existing_key = next((key for key, bc in current_ini_content.items() if bc["id"] == constraint.id), None) - if not existing_key: - raise ConstraintDoesNotExistError(constraint.name) - - existing_constraint = current_ini_content[existing_key] + existing_constraint = self._get_constraint_inside_ini(current_ini_content, constraint) new_terms = {term.id: term.weight_offset() for term in terms} new_content = existing_constraint | new_terms self.ini_file.ini_dict = new_content @@ -166,13 +161,8 @@ def update_binding_constraint_properties( self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate ) -> BindingConstraintProperties: current_ini_content = self.ini_file.ini_dict - # Ensures the constraint already exists - existing_key = next((key for key, bc in current_ini_content.items() if bc["id"] == binding_constraint.id), None) - if not existing_key: - raise ConstraintDoesNotExistError(binding_constraint.name) - + existing_constraint = self._get_constraint_inside_ini(current_ini_content, binding_constraint) local_properties = BindingConstraintPropertiesLocal.from_user_model(properties) - existing_constraint = current_ini_content[existing_key] existing_constraint.update(local_properties.model_dump(mode="json", by_alias=True)) self.ini_file.ini_dict = current_ini_content self.ini_file.write_ini_file() @@ -194,3 +184,11 @@ def update_constraint_matrix( @override def read_binding_constraints(self) -> list[BindingConstraint]: raise NotImplementedError + + @staticmethod + def _get_constraint_inside_ini(ini_content: dict[str, Any], constraint: BindingConstraint) -> dict[str, Any]: + existing_key = next((key for key, bc in ini_content.items() if bc["id"] == constraint.id), None) + if not existing_key: + raise ConstraintDoesNotExistError(constraint.name) + + return ini_content[existing_key] From 41beadd14afb0d383ecc62be85380934804951be Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:09:34 +0100 Subject: [PATCH 16/27] put method in common --- .../craft/service/local_services/services/binding_constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index bc5092f4..bbaf4dd7 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -191,4 +191,4 @@ def _get_constraint_inside_ini(ini_content: dict[str, Any], constraint: BindingC if not existing_key: raise ConstraintDoesNotExistError(constraint.name) - return ini_content[existing_key] + return ini_content[existing_key] # type: ignore From dfd4856960b3e08e57cab000658bca355e15d857 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:12:55 +0100 Subject: [PATCH 17/27] continue --- .../service/local_services/services/binding_constraint.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index bbaf4dd7..f75e445d 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -148,8 +148,8 @@ def add_constraint_terms(self, constraint: BindingConstraint, terms: list[Constr current_ini_content = self.ini_file.ini_dict_binding_constraints or {} existing_constraint = self._get_constraint_inside_ini(current_ini_content, constraint) new_terms = {term.id: term.weight_offset() for term in terms} - new_content = existing_constraint | new_terms - self.ini_file.ini_dict = new_content + existing_constraint.update(new_terms) + self.ini_file.ini_dict = current_ini_content self.ini_file.write_ini_file() @override From 8ecdd299f4577862bf65e490da820aa033beadd8 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:19:18 +0100 Subject: [PATCH 18/27] fixing tests --- .../services/local_services/test_study.py | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 07e1d4f3..04a2cbc4 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1214,15 +1214,8 @@ def test_duplicate_name_errors(self, local_study_with_constraint): ): local_study_with_constraint.create_binding_constraint(name=binding_constraint_name) - def test_constraints_have_default_properties(self, local_study_with_constraint): - # Given - constraint = local_study_with_constraint.get_binding_constraints()["test constraint"] - - # Then - assert constraint.properties.model_dump(exclude_none=True) - - def test_constraints_have_correct_default_properties(self, test_constraint, default_constraint_properties): - assert test_constraint.properties == default_constraint_properties + def test_constraints_have_correct_default_properties(self, test_constraint): + assert test_constraint.properties == BindingConstraintProperties() def test_creating_constraints_creates_ini(self, local_study_with_constraint): # Given @@ -1239,11 +1232,12 @@ def test_constraints_ini_have_correct_default_content( ): # Given expected_ini_contents = """[0] -name = test constraint id = test constraint -enabled = true +name = test constraint +enabled = True type = hourly operator = less +comments = filter-year-by-year = hourly filter-synthesis = hourly group = default @@ -1274,19 +1268,20 @@ def test_constraints_and_ini_have_custom_properties(self, local_study_with_const group="test group", ) expected_ini_content = """[0] -name = test constraint id = test constraint -enabled = true +name = test constraint +enabled = True type = hourly operator = less +comments = filter-year-by-year = hourly filter-synthesis = hourly group = default [1] -name = test constraint two id = test constraint two -enabled = false +name = test constraint two +enabled = False type = weekly operator = both comments = test comment @@ -1318,11 +1313,12 @@ def test_constraint_can_add_term(self, test_constraint): def test_constraint_term_and_ini_have_correct_defaults(self, local_study_with_constraint, test_constraint): # Given expected_ini_contents = """[0] -name = test constraint id = test constraint -enabled = true +name = test constraint +enabled = True type = hourly operator = less +comments = filter-year-by-year = hourly filter-synthesis = hourly group = default @@ -1342,11 +1338,12 @@ def test_constraint_term_with_offset_and_ini_have_correct_values( ): # Given expected_ini_contents = """[0] -name = test constraint id = test constraint -enabled = true +name = test constraint +enabled = True type = hourly operator = less +comments = filter-year-by-year = hourly filter-synthesis = hourly group = default From 5c566a0bba6c3d38c9ba757e150a4d1669dabe4a Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:21:35 +0100 Subject: [PATCH 19/27] fix test --- tests/antares/integration/test_local_client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/antares/integration/test_local_client.py b/tests/antares/integration/test_local_client.py index d9b23db8..e0a85316 100644 --- a/tests/antares/integration/test_local_client.py +++ b/tests/antares/integration/test_local_client.py @@ -18,7 +18,13 @@ from antares.craft.exceptions.exceptions import AreaCreationError, LinkCreationError from antares.craft.model.area import AdequacyPatchMode, Area, AreaProperties, AreaUi -from antares.craft.model.binding_constraint import BindingConstraintProperties, ClusterData, ConstraintTerm, LinkData +from antares.craft.model.binding_constraint import ( + BindingConstraintOperator, + BindingConstraintProperties, + ClusterData, + ConstraintTerm, + LinkData, +) from antares.craft.model.commons import FilterOption from antares.craft.model.link import Link, LinkProperties, LinkUi from antares.craft.model.renewable import RenewableClusterGroup, RenewableClusterProperties @@ -240,7 +246,7 @@ def test_local_study(self, tmp_path: Path, unknown_area): assert constraint_1.get_terms() == {link_term_1.id: link_term_1, cluster_term.id: cluster_term} # Case that succeeds - properties = BindingConstraintProperties(operator="less") + properties = BindingConstraintProperties(operator=BindingConstraintOperator.LESS) matrix = pd.DataFrame(data=(np.ones((8784, 1)))) constraint_3 = test_study.create_binding_constraint(name="bc_3", less_term_matrix=matrix, properties=properties) assert constraint_3.get_less_term_matrix().equals(matrix) From e644caa678035cecfc6cfbf7be34bca12efe5d39 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Wed, 12 Feb 2025 18:25:10 +0100 Subject: [PATCH 20/27] fix integration test --- tests/integration/test_web_client.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index 3dd47474..e16d68ee 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -32,6 +32,7 @@ BindingConstraintFrequency, BindingConstraintOperator, BindingConstraintProperties, + BindingConstraintPropertiesUpdate, ClusterData, ConstraintTerm, LinkData, @@ -381,7 +382,7 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): constraint_2.update_equal_term_matrix(wrong_matrix) # Case that succeeds - properties = BindingConstraintProperties(operator="less") + properties = BindingConstraintProperties(operator=BindingConstraintOperator.LESS) matrix = pd.DataFrame(data=(np.ones((8784, 1)))) constraint_3 = study.create_binding_constraint(name="bc_3", less_term_matrix=matrix, properties=properties) assert constraint_3.get_less_term_matrix().equals(matrix) @@ -389,8 +390,8 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): # test update constraint matrices new_matrix = pd.DataFrame(data=(np.ones((8784, 1)))) new_matrix.iloc[0, 0] = 72 - properties.operator = "equal" - constraint_3.update_properties(properties) + update_properties = BindingConstraintPropertiesUpdate(operator=BindingConstraintOperator.EQUAL) + constraint_3.update_properties(update_properties) constraint_3.update_equal_term_matrix(new_matrix) assert constraint_3.get_equal_term_matrix().equals(new_matrix) @@ -462,10 +463,10 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): assert battery_fr.properties.reservoir_capacity == 0.5 # Checks old value wasn't modified # tests constraint properties update - new_props = BindingConstraintProperties() - new_props.group = "another_group" + new_props = BindingConstraintPropertiesUpdate(group="another_group") constraint_1.update_properties(new_props) assert constraint_1.properties.group == "another_group" + assert constraint_1.properties.enabled is False # Checks old value wasn't modified # tests constraint deletion study.delete_binding_constraint(constraint_1) From 131b2b3140e12a0005651b087b56fa5b1ea0cf99 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 10:32:36 +0100 Subject: [PATCH 21/27] terms --- src/antares/craft/model/binding_constraint.py | 70 ++++++++++--------- .../services/binding_constraint.py | 41 ++++++----- .../services/local_services/test_study.py | 11 +-- 3 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index b416286f..86f55aad 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -11,13 +11,12 @@ # This file is part of the Antares project. from dataclasses import dataclass from enum import Enum -from typing import Any, Optional, Union +from typing import Optional, Union import pandas as pd from antares.craft.service.base_services import BaseBindingConstraintService from antares.craft.tools.contents_tool import EnumIgnoreCase, transform_name_to_id -from pydantic import BaseModel, Field, model_validator class BindingConstraintFrequency(EnumIgnoreCase): @@ -39,20 +38,8 @@ class ConstraintMatrixName(Enum): GREATER_TERM = "gt" -class TermOperators(BaseModel): - weight: Optional[float] = None - offset: Optional[int] = None - - def weight_offset(self) -> str: - if self.offset is not None: - # Rounded the weight to 6 decimals to be in line with other floats in the ini files - weight_offset = f"{(self.weight if self.weight is not None else 0):.6f}%{self.offset}" - else: - weight_offset = f"{self.weight if self.weight is not None else 0}" - return weight_offset - - -class LinkData(BaseModel): +@dataclass +class LinkData: """ DTO for a constraint term on a link between two areas. """ @@ -61,7 +48,8 @@ class LinkData(BaseModel): area2: str -class ClusterData(BaseModel): +@dataclass +class ClusterData: """ DTO for a constraint term on a cluster in an area. """ @@ -70,24 +58,38 @@ class ClusterData(BaseModel): cluster: str -class ConstraintTerm(TermOperators): +@dataclass +class ConstraintTermData: data: Union[LinkData, ClusterData] - id: str = Field(init=False) - - @model_validator(mode="before") - def fill_id(cls, v: dict[str, Any]) -> dict[str, Any]: - v["id"] = cls.generate_id(v["data"]) - return v - - @classmethod - def generate_id(cls, data: Union[dict[str, str], LinkData, ClusterData]) -> str: - if isinstance(data, dict): - if "area1" in data: - return "%".join(sorted((data["area1"].lower(), data["area2"].lower()))) - return ".".join((data["area"].lower(), data["cluster"].lower())) - elif isinstance(data, LinkData): - return "%".join(sorted((data.area1.lower(), data.area2.lower()))) - return ".".join((data.area.lower(), data.cluster.lower())) + + @property + def id(self) -> str: + if isinstance(self.data, LinkData): + return "%".join(sorted((self.data.area1.lower(), self.data.area2.lower()))) + return ".".join((self.data.area.lower(), self.data.cluster.lower())) + + @staticmethod + def from_dict(input: dict[str, str]) -> Union[LinkData, ClusterData]: + if "area1" in input: + return LinkData(area1=input["area1"], area2=input["area2"]) + elif "cluster" in input: + return ClusterData(area=input["area"], cluster=input["cluster"]) + raise ValueError(f"Dict {input} couldn't be serialized as a ConstraintTermData object") + + +@dataclass +class ConstraintTerm(ConstraintTermData): + weight: float = 1 + offset: int = 0 + + def weight_offset(self) -> str: + return f"{self.weight}%{self.offset}" if self.offset != 0 else f"{self.weight}" + + +@dataclass +class ConstraintTermUpdate(ConstraintTermData): + weight: Optional[float] = None + offset: Optional[int] = None @dataclass diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index 8e71c4ba..ae9c1fc1 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +from dataclasses import asdict from pathlib import PurePosixPath from typing import Optional @@ -33,6 +33,7 @@ BindingConstraintPropertiesUpdate, ConstraintMatrixName, ConstraintTerm, + ConstraintTermData, ) from antares.craft.service.api_services.models.binding_constraint import BindingConstraintPropertiesAPI from antares.craft.service.api_services.utils import get_matrix @@ -95,22 +96,18 @@ def create_binding_constraint( del created_properties[key] api_properties = BindingConstraintPropertiesAPI.model_validate(created_properties) bc_properties = api_properties.to_user_model() - bc_terms: list[ConstraintTerm] = [] if terms: - json_terms = [term.model_dump() for term in terms] + json_terms = [ + {"weight": term.weight, "offset": term.offset, "data": asdict(term.data)} for term in terms + ] url = f"{base_url}/{bc_id}/terms" self._wrapper.post(url, json=json_terms) - url = f"{base_url}/{bc_id}" - response = self._wrapper.get(url) - created_terms = response.json()["terms"] - bc_terms = [ConstraintTerm.model_validate(term) for term in created_terms] - except APIError as e: raise BindingConstraintCreationError(name, e.message) from e - constraint = BindingConstraint(name, self, bc_properties, bc_terms) + constraint = BindingConstraint(name, self, bc_properties, terms) return constraint @@ -173,7 +170,7 @@ def update_constraint_matrix( def add_constraint_terms(self, constraint: BindingConstraint, terms: list[ConstraintTerm]) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint.id}" try: - json_terms = [term.model_dump() for term in terms] + json_terms = [{"weight": term.weight, "offset": term.offset, "data": asdict(term.data)} for term in terms] self._wrapper.post(f"{url}/terms", json=json_terms) except APIError as e: @@ -185,18 +182,20 @@ def read_binding_constraints(self) -> list[BindingConstraint]: try: response = self._wrapper.get(url) constraints_json = response.json() + constraints = [] + + for constraint in constraints_json: + constraint_name = constraint.pop("name") + del constraint["id"] + api_terms = constraint.pop("terms") + api_properties = BindingConstraintPropertiesAPI.model_validate(constraint) + bc_properties = api_properties.to_user_model() + terms: list[ConstraintTerm] = [] + for api_term in api_terms: + term_data = ConstraintTermData.from_dict(api_term["data"]) + terms.append(ConstraintTerm(weight=api_term["weight"], offset=api_term["offset"], data=term_data)) + constraints.append(BindingConstraint(constraint_name, self, bc_properties, terms)) - constraints = [ - BindingConstraint( - constraint["name"], - self, - BindingConstraintPropertiesAPI.model_validate( - {k: v for k, v in constraint.items() if k not in ["terms", "id", "name"]} - ).to_user_model(), - [ConstraintTerm.model_validate(term) for term in constraint["terms"]], - ) - for constraint in constraints_json - ] constraints.sort(key=lambda constraint: constraint.id) return constraints except APIError as e: diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 04a2cbc4..4be5b471 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -36,6 +36,7 @@ BindingConstraintOperator, BindingConstraintProperties, ConstraintTerm, + LinkData, ) from antares.craft.model.commons import FilterOption from antares.craft.model.hydro import Hydro @@ -1306,7 +1307,7 @@ def test_constraints_and_ini_have_custom_properties(self, local_study_with_const assert actual_ini_content == expected_ini_content def test_constraint_can_add_term(self, test_constraint): - new_term = [ConstraintTerm(data={"area1": "fr", "area2": "at"})] + new_term = [ConstraintTerm(data=LinkData(area1="fr", area2="at"))] test_constraint.add_terms(new_term) assert test_constraint.get_terms() @@ -1322,11 +1323,11 @@ def test_constraint_term_and_ini_have_correct_defaults(self, local_study_with_co filter-year-by-year = hourly filter-synthesis = hourly group = default -at%fr = 0 +at%fr = 1 """ # When - new_term = [ConstraintTerm(data={"area1": "fr", "area2": "at"})] + new_term = [ConstraintTerm(data=LinkData(area1="fr", area2="at"))] test_constraint.add_terms(new_term) with local_study_with_constraint._binding_constraints_service.ini_file.ini_path.open("r") as file: actual_ini_content = file.read() @@ -1347,11 +1348,11 @@ def test_constraint_term_with_offset_and_ini_have_correct_values( filter-year-by-year = hourly filter-synthesis = hourly group = default -at%fr = 0.000000%1 +at%fr = 1%1 """ # When - new_term = [ConstraintTerm(offset=1, data={"area1": "fr", "area2": "at"})] + new_term = [ConstraintTerm(offset=1, data=LinkData(area1="fr", area2="at"))] test_constraint.add_terms(new_term) with local_study_with_constraint._binding_constraints_service.ini_file.ini_path.open("r") as file: actual_ini_content = file.read() From 389f2c63bc68c59c142628add9126ee0a1205285 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 10:39:49 +0100 Subject: [PATCH 22/27] remove useless thing --- src/antares/craft/service/base_services.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index e0319171..69143b0f 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -494,8 +494,6 @@ def read_thermal_clusters(self, area_id: str) -> list["ThermalCluster"]: class BaseBindingConstraintService(ABC): - binding_constraints: dict[str, "BindingConstraint"] - @abstractmethod def create_binding_constraint( self, From 567abef8f03ada754d9fb6eb40836fa83b6e2b18 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 10:51:01 +0100 Subject: [PATCH 23/27] add update term method --- src/antares/craft/exceptions/exceptions.py | 6 ++++++ src/antares/craft/model/binding_constraint.py | 18 +++++++++++++----- .../services/binding_constraint.py | 19 ++++++++++++++++++- src/antares/craft/service/base_services.py | 10 ++++++++++ .../services/binding_constraint.py | 5 +++++ 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/antares/craft/exceptions/exceptions.py b/src/antares/craft/exceptions/exceptions.py index 501a58fc..554b4882 100644 --- a/src/antares/craft/exceptions/exceptions.py +++ b/src/antares/craft/exceptions/exceptions.py @@ -244,6 +244,12 @@ def __init__(self, constraint_id: str, term_id: str, message: str) -> None: super().__init__(self.message) +class ConstraintTermEditionError(Exception): + def __init__(self, constraint_id: str, term_id: str, message: str) -> None: + self.message = f"Could not update the term {term_id} of the binding constraint {constraint_id}: " + message + super().__init__(self.message) + + class StudyCreationError(Exception): def __init__(self, study_name: str, message: str) -> None: self.message = f"Could not create the study {study_name}: " + message diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index 86f55aad..f8f754f5 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -77,6 +77,12 @@ def from_dict(input: dict[str, str]) -> Union[LinkData, ClusterData]: raise ValueError(f"Dict {input} couldn't be serialized as a ConstraintTermData object") +@dataclass +class ConstraintTermUpdate(ConstraintTermData): + weight: Optional[float] = None + offset: Optional[int] = None + + @dataclass class ConstraintTerm(ConstraintTermData): weight: float = 1 @@ -85,11 +91,9 @@ class ConstraintTerm(ConstraintTermData): def weight_offset(self) -> str: return f"{self.weight}%{self.offset}" if self.offset != 0 else f"{self.weight}" - -@dataclass -class ConstraintTermUpdate(ConstraintTermData): - weight: Optional[float] = None - offset: Optional[int] = None + @staticmethod + def from_update_model(update_model: ConstraintTermUpdate) -> "ConstraintTerm": + return ConstraintTerm(data=update_model.data, weight=update_model.weight or 1, offset=update_model.offset or 0) @dataclass @@ -152,6 +156,10 @@ def delete_term(self, term: ConstraintTerm) -> None: self._binding_constraint_service.delete_binding_constraint_term(self.id, term.id) self._terms.pop(term.id) + def update_term(self, term: ConstraintTermUpdate) -> None: + new_term = self._binding_constraint_service.update_binding_constraint_term(self.id, term) + self._terms[new_term.id] = new_term + def update_properties(self, properties: BindingConstraintPropertiesUpdate) -> None: new_properties = self._binding_constraint_service.update_binding_constraint_properties(self, properties) self._properties = new_properties diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index ae9c1fc1..6d70c60f 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -11,7 +11,7 @@ # This file is part of the Antares project. from dataclasses import asdict from pathlib import PurePosixPath -from typing import Optional +from typing import Any, Optional import pandas as pd @@ -26,6 +26,7 @@ ConstraintRetrievalError, ConstraintTermAdditionError, ConstraintTermDeletionError, + ConstraintTermEditionError, ) from antares.craft.model.binding_constraint import ( BindingConstraint, @@ -34,6 +35,7 @@ ConstraintMatrixName, ConstraintTerm, ConstraintTermData, + ConstraintTermUpdate, ) from antares.craft.service.api_services.models.binding_constraint import BindingConstraintPropertiesAPI from antares.craft.service.api_services.utils import get_matrix @@ -119,6 +121,21 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No except APIError as e: raise ConstraintTermDeletionError(constraint_id, term_id, e.message) from e + @override + def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> ConstraintTerm: + url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint_id}/term" + try: + body: dict[str, Any] = {"data": asdict(term.data)} + if term.offset: + body["offset"] = term.offset + if term.weight: + body["weight"] = term.weight + self._wrapper.put(url, json=body) + except APIError as e: + raise ConstraintTermEditionError(constraint_id, term.id, e.message) from e + + return ConstraintTerm.from_update_model(term) + @override def update_binding_constraint_properties( self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index 69143b0f..cfe60f96 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -28,6 +28,7 @@ BindingConstraintPropertiesUpdate, ConstraintMatrixName, ConstraintTerm, + ConstraintTermUpdate, ) from antares.craft.model.hydro import HydroProperties, HydroPropertiesUpdate from antares.craft.model.link import Link, LinkProperties, LinkUi @@ -539,6 +540,15 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No """ pass + @abstractmethod + def update_binding_constraint_term(self, constraint_id: str, term: "ConstraintTermUpdate") -> "ConstraintTerm": + """ + Args: + constraint_id: binding constraint's id containing the term + term: binding constraint term to be updated + """ + pass + @abstractmethod def update_binding_constraint_properties( self, binding_constraint: "BindingConstraint", properties: "BindingConstraintPropertiesUpdate" diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index f75e445d..117babde 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -27,6 +27,7 @@ BindingConstraintPropertiesUpdate, ConstraintMatrixName, ConstraintTerm, + ConstraintTermUpdate, ) from antares.craft.service.base_services import BaseBindingConstraintService from antares.craft.service.local_services.models.binding_constraint import BindingConstraintPropertiesLocal @@ -156,6 +157,10 @@ def add_constraint_terms(self, constraint: BindingConstraint, terms: list[Constr def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> None: raise NotImplementedError + @override + def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> ConstraintTerm: + raise NotImplementedError + @override def update_binding_constraint_properties( self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate From 838a0c0569acc039380da330ef5f166199dd28f2 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 10:58:34 +0100 Subject: [PATCH 24/27] write test --- src/antares/craft/model/binding_constraint.py | 12 ++++++----- .../services/binding_constraint.py | 4 +--- src/antares/craft/service/base_services.py | 2 +- .../services/binding_constraint.py | 2 +- .../test_binding_constraint_api.py | 21 +++++++++++++++++-- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index f8f754f5..0a868201 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -91,9 +91,10 @@ class ConstraintTerm(ConstraintTermData): def weight_offset(self) -> str: return f"{self.weight}%{self.offset}" if self.offset != 0 else f"{self.weight}" - @staticmethod - def from_update_model(update_model: ConstraintTermUpdate) -> "ConstraintTerm": - return ConstraintTerm(data=update_model.data, weight=update_model.weight or 1, offset=update_model.offset or 0) + def from_update_model(self, update_model: ConstraintTermUpdate) -> "ConstraintTerm": + return ConstraintTerm( + data=self.data, weight=update_model.weight or self.weight, offset=update_model.offset or self.offset + ) @dataclass @@ -157,8 +158,9 @@ def delete_term(self, term: ConstraintTerm) -> None: self._terms.pop(term.id) def update_term(self, term: ConstraintTermUpdate) -> None: - new_term = self._binding_constraint_service.update_binding_constraint_term(self.id, term) - self._terms[new_term.id] = new_term + self._binding_constraint_service.update_binding_constraint_term(self.id, term) + existing_term = self._terms[term.id] + self._terms[term.id] = existing_term.from_update_model(term) def update_properties(self, properties: BindingConstraintPropertiesUpdate) -> None: new_properties = self._binding_constraint_service.update_binding_constraint_properties(self, properties) diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index 6d70c60f..cc324536 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -122,7 +122,7 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No raise ConstraintTermDeletionError(constraint_id, term_id, e.message) from e @override - def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> ConstraintTerm: + def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint_id}/term" try: body: dict[str, Any] = {"data": asdict(term.data)} @@ -134,8 +134,6 @@ def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTer except APIError as e: raise ConstraintTermEditionError(constraint_id, term.id, e.message) from e - return ConstraintTerm.from_update_model(term) - @override def update_binding_constraint_properties( self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index cfe60f96..34dcffb7 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -541,7 +541,7 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No pass @abstractmethod - def update_binding_constraint_term(self, constraint_id: str, term: "ConstraintTermUpdate") -> "ConstraintTerm": + def update_binding_constraint_term(self, constraint_id: str, term: "ConstraintTermUpdate") -> None: """ Args: constraint_id: binding constraint's id containing the term diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index 117babde..c0cbae8d 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -158,7 +158,7 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No raise NotImplementedError @override - def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> ConstraintTerm: + def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> None: raise NotImplementedError @override diff --git a/tests/antares/services/api_services/test_binding_constraint_api.py b/tests/antares/services/api_services/test_binding_constraint_api.py index c46887d7..6523aab8 100644 --- a/tests/antares/services/api_services/test_binding_constraint_api.py +++ b/tests/antares/services/api_services/test_binding_constraint_api.py @@ -23,6 +23,9 @@ BindingConstraintProperties, BindingConstraintPropertiesUpdate, ConstraintMatrixName, + ConstraintTerm, + ConstraintTermUpdate, + LinkData, ) from antares.craft.model.study import Study from antares.craft.service.api_services.models.binding_constraint import BindingConstraintPropertiesAPI @@ -73,7 +76,7 @@ def test_update_binding_constraint_properties_success(self): def test_update_binding_constraint_properties_fails(self): with requests_mock.Mocker() as mocker: - properties = BindingConstraintProperties(enabled=False) + update_properties = BindingConstraintPropertiesUpdate(enabled=False) constraint = BindingConstraint( "bc_1", ServiceFactory(self.api, self.study_id).create_binding_constraints_service() ) @@ -85,7 +88,21 @@ def test_update_binding_constraint_properties_fails(self): ConstraintPropertiesUpdateError, match=f"Could not update properties for binding constraint {constraint.id}: {antares_web_description_msg}", ): - constraint.update_properties(properties=properties) + constraint.update_properties(properties=update_properties) + + def test_update_binding_constraint_terms_success(self): + with requests_mock.Mocker() as mocker: + existing_term = ConstraintTerm(data=LinkData(area1="fr", area2="be"), weight=4, offset=3) + service = ServiceFactory(self.api, self.study_id).create_binding_constraints_service() + constraint = BindingConstraint("bc_1", service, None, [existing_term]) + + url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint.id}/term" + mocker.put(url, status_code=200) + + new_term = ConstraintTermUpdate(data=LinkData(area1="fr", area2="be"), weight=2) + constraint.update_term(new_term) + updated_term = constraint.get_terms()[existing_term.id] + assert updated_term == ConstraintTerm(data=LinkData(area1="fr", area2="be"), weight=2, offset=3) def test_get_constraint_matrix_success(self, constraint_set): constraint = BindingConstraint( From 081b68d80a9444b766e07007232f416500b62fe8 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 11:01:06 +0100 Subject: [PATCH 25/27] add unit test --- .../test_binding_constraint_api.py | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/tests/antares/services/api_services/test_binding_constraint_api.py b/tests/antares/services/api_services/test_binding_constraint_api.py index 6523aab8..540c1712 100644 --- a/tests/antares/services/api_services/test_binding_constraint_api.py +++ b/tests/antares/services/api_services/test_binding_constraint_api.py @@ -16,7 +16,11 @@ import pandas as pd from antares.craft.api_conf.api_conf import APIconf -from antares.craft.exceptions.exceptions import ConstraintMatrixDownloadError, ConstraintPropertiesUpdateError +from antares.craft.exceptions.exceptions import ( + ConstraintMatrixDownloadError, + ConstraintPropertiesUpdateError, + ConstraintTermEditionError, +) from antares.craft.model.area import Area from antares.craft.model.binding_constraint import ( BindingConstraint, @@ -90,7 +94,7 @@ def test_update_binding_constraint_properties_fails(self): ): constraint.update_properties(properties=update_properties) - def test_update_binding_constraint_terms_success(self): + def test_update_binding_constraint_term_success(self): with requests_mock.Mocker() as mocker: existing_term = ConstraintTerm(data=LinkData(area1="fr", area2="be"), weight=4, offset=3) service = ServiceFactory(self.api, self.study_id).create_binding_constraints_service() @@ -104,6 +108,21 @@ def test_update_binding_constraint_terms_success(self): updated_term = constraint.get_terms()[existing_term.id] assert updated_term == ConstraintTerm(data=LinkData(area1="fr", area2="be"), weight=2, offset=3) + def test_update_binding_constraint_term_fails(self): + with requests_mock.Mocker() as mocker: + service = ServiceFactory(self.api, self.study_id).create_binding_constraints_service() + constraint = BindingConstraint("bc_1", service) + + url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint.id}/term" + mocker.put(url, json={"description": self.antares_web_description_msg}, status_code=422) + + new_term = ConstraintTermUpdate(data=LinkData(area1="fr", area2="be"), weight=2) + with pytest.raises( + ConstraintTermEditionError, + match=f"Could not update the term {new_term.id} of the binding constraint {constraint.id}: {self.antares_web_description_msg}", + ): + constraint.update_term(new_term) + def test_get_constraint_matrix_success(self, constraint_set): constraint = BindingConstraint( "bc_test", ServiceFactory(self.api, self.study_id).create_binding_constraints_service() From 4bc210903065ac1e89c81f86a5a05832288c87d0 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 11:20:17 +0100 Subject: [PATCH 26/27] add tests --- src/antares/craft/model/binding_constraint.py | 9 +++++++-- .../service/api_services/services/binding_constraint.py | 4 +--- .../services/api_services/test_binding_constraint_api.py | 3 ++- tests/integration/test_web_client.py | 8 ++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index 0a868201..8107c31f 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -158,9 +158,14 @@ def delete_term(self, term: ConstraintTerm) -> None: self._terms.pop(term.id) def update_term(self, term: ConstraintTermUpdate) -> None: - self._binding_constraint_service.update_binding_constraint_term(self.id, term) existing_term = self._terms[term.id] - self._terms[term.id] = existing_term.from_update_model(term) + if not term.weight: + term.weight = existing_term.weight + self._binding_constraint_service.update_binding_constraint_term(self.id, term) + if term.offset: + existing_term.offset = term.offset + existing_term.weight = term.weight + self._terms[term.id] = existing_term def update_properties(self, properties: BindingConstraintPropertiesUpdate) -> None: new_properties = self._binding_constraint_service.update_binding_constraint_properties(self, properties) diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index cc324536..db4105e3 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -125,11 +125,9 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> None: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint_id}/term" try: - body: dict[str, Any] = {"data": asdict(term.data)} + body: dict[str, Any] = {"data": asdict(term.data), "weight": term.weight} if term.offset: body["offset"] = term.offset - if term.weight: - body["weight"] = term.weight self._wrapper.put(url, json=body) except APIError as e: raise ConstraintTermEditionError(constraint_id, term.id, e.message) from e diff --git a/tests/antares/services/api_services/test_binding_constraint_api.py b/tests/antares/services/api_services/test_binding_constraint_api.py index 540c1712..76967973 100644 --- a/tests/antares/services/api_services/test_binding_constraint_api.py +++ b/tests/antares/services/api_services/test_binding_constraint_api.py @@ -110,8 +110,9 @@ def test_update_binding_constraint_term_success(self): def test_update_binding_constraint_term_fails(self): with requests_mock.Mocker() as mocker: + existing_term = ConstraintTerm(data=LinkData(area1="fr", area2="be"), weight=4, offset=3) service = ServiceFactory(self.api, self.study_id).create_binding_constraints_service() - constraint = BindingConstraint("bc_1", service) + constraint = BindingConstraint("bc_1", service, None, [existing_term]) url = f"https://antares.com/api/v1/studies/{self.study_id}/bindingconstraints/{constraint.id}/term" mocker.put(url, json={"description": self.antares_web_description_msg}, status_code=422) diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index e16d68ee..1f11f098 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -35,6 +35,7 @@ BindingConstraintPropertiesUpdate, ClusterData, ConstraintTerm, + ConstraintTermUpdate, LinkData, ) from antares.craft.model.hydro import HydroPropertiesUpdate @@ -411,6 +412,13 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop, tmp_path): constraint_3.id: constraint_3, } + # tests updating an existing term + new_term = ConstraintTermUpdate(data=cluster_data, offset=12) + constraint_1.update_term(new_term) + updated_term = constraint_1.get_terms()[new_term.id] + assert updated_term.weight == 100 # Checks the weight wasn't modified + assert updated_term.offset == 12 + # test area property edition new_props = AreaProperties() new_props.adequacy_patch_mode = AdequacyPatchMode.VIRTUAL From 0dfb230afc19da4baae991846a9080f523a4ed2e Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 13 Feb 2025 11:37:16 +0100 Subject: [PATCH 27/27] add tests --- src/antares/craft/model/binding_constraint.py | 14 ++------------ .../api_services/services/binding_constraint.py | 12 ++++++++++-- src/antares/craft/service/base_services.py | 7 +++++-- .../local_services/services/binding_constraint.py | 4 +++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/antares/craft/model/binding_constraint.py b/src/antares/craft/model/binding_constraint.py index 8107c31f..a1753c39 100644 --- a/src/antares/craft/model/binding_constraint.py +++ b/src/antares/craft/model/binding_constraint.py @@ -91,11 +91,6 @@ class ConstraintTerm(ConstraintTermData): def weight_offset(self) -> str: return f"{self.weight}%{self.offset}" if self.offset != 0 else f"{self.weight}" - def from_update_model(self, update_model: ConstraintTermUpdate) -> "ConstraintTerm": - return ConstraintTerm( - data=self.data, weight=update_model.weight or self.weight, offset=update_model.offset or self.offset - ) - @dataclass class BindingConstraintPropertiesUpdate: @@ -159,13 +154,8 @@ def delete_term(self, term: ConstraintTerm) -> None: def update_term(self, term: ConstraintTermUpdate) -> None: existing_term = self._terms[term.id] - if not term.weight: - term.weight = existing_term.weight - self._binding_constraint_service.update_binding_constraint_term(self.id, term) - if term.offset: - existing_term.offset = term.offset - existing_term.weight = term.weight - self._terms[term.id] = existing_term + new_term = self._binding_constraint_service.update_binding_constraint_term(self.id, term, existing_term) + self._terms[term.id] = new_term def update_properties(self, properties: BindingConstraintPropertiesUpdate) -> None: new_properties = self._binding_constraint_service.update_binding_constraint_properties(self, properties) diff --git a/src/antares/craft/service/api_services/services/binding_constraint.py b/src/antares/craft/service/api_services/services/binding_constraint.py index db4105e3..8c49bb3d 100644 --- a/src/antares/craft/service/api_services/services/binding_constraint.py +++ b/src/antares/craft/service/api_services/services/binding_constraint.py @@ -122,16 +122,24 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No raise ConstraintTermDeletionError(constraint_id, term_id, e.message) from e @override - def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> None: + def update_binding_constraint_term( + self, constraint_id: str, term: ConstraintTermUpdate, existing_term: ConstraintTerm + ) -> ConstraintTerm: url = f"{self._base_url}/studies/{self.study_id}/bindingconstraints/{constraint_id}/term" try: - body: dict[str, Any] = {"data": asdict(term.data), "weight": term.weight} + body: dict[str, Any] = {"data": asdict(term.data), "weight": term.weight or existing_term.weight} if term.offset: body["offset"] = term.offset self._wrapper.put(url, json=body) except APIError as e: raise ConstraintTermEditionError(constraint_id, term.id, e.message) from e + if term.weight: + existing_term.weight = term.weight + if term.offset: + existing_term.offset = term.offset + return existing_term + @override def update_binding_constraint_properties( self, binding_constraint: BindingConstraint, properties: BindingConstraintPropertiesUpdate diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index 34dcffb7..38aa0291 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -541,11 +541,14 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No pass @abstractmethod - def update_binding_constraint_term(self, constraint_id: str, term: "ConstraintTermUpdate") -> None: + def update_binding_constraint_term( + self, constraint_id: str, term: "ConstraintTermUpdate", existing_term: "ConstraintTerm" + ) -> "ConstraintTerm": """ Args: constraint_id: binding constraint's id containing the term - term: binding constraint term to be updated + term: term with new values + existing_term: existing term with existing values """ pass diff --git a/src/antares/craft/service/local_services/services/binding_constraint.py b/src/antares/craft/service/local_services/services/binding_constraint.py index c0cbae8d..a59dc171 100644 --- a/src/antares/craft/service/local_services/services/binding_constraint.py +++ b/src/antares/craft/service/local_services/services/binding_constraint.py @@ -158,7 +158,9 @@ def delete_binding_constraint_term(self, constraint_id: str, term_id: str) -> No raise NotImplementedError @override - def update_binding_constraint_term(self, constraint_id: str, term: ConstraintTermUpdate) -> None: + def update_binding_constraint_term( + self, constraint_id: str, term: ConstraintTermUpdate, existing_term: ConstraintTerm + ) -> ConstraintTerm: raise NotImplementedError @override