From 5142aa3ca7df3f3510c093db19df00a955d774bb Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Sun, 24 Mar 2024 19:06:54 +0100 Subject: [PATCH] feat(bc): used camelCase field names in binding constraints API --- .../business/binding_constraint_management.py | 70 +++++++++++-------- .../command/create_binding_constraint.py | 12 ++-- antarest/study/web/study_data_blueprint.py | 23 +++++- .../test_binding_constraints.py | 68 +++++++++--------- tests/integration/test_integration.py | 2 +- .../Commands/Edition/commandTypes.ts | 4 +- .../BindingConstraints/AddDialog.tsx | 4 +- .../BindingConstView/BindingConstForm.tsx | 2 +- .../BindingConstView/utils.ts | 12 ++-- 9 files changed, 113 insertions(+), 84 deletions(-) diff --git a/antarest/study/business/binding_constraint_management.py b/antarest/study/business/binding_constraint_management.py index e29d4cad5e..265ee35828 100644 --- a/antarest/study/business/binding_constraint_management.py +++ b/antarest/study/business/binding_constraint_management.py @@ -1,10 +1,10 @@ import collections import itertools import logging -from typing import Any, Dict, List, Mapping, Optional, Sequence, Union +from typing import Any, Dict, List, Mapping, MutableSequence, Optional, Sequence, Union import numpy as np -from pydantic import BaseModel, root_validator, validator +from pydantic import BaseModel, Field, root_validator, validator from requests.utils import CaseInsensitiveDict from antarest.core.exceptions import ( @@ -19,7 +19,8 @@ MissingDataError, NoConstraintError, ) -from antarest.study.business.utils import AllOptionalMetaclass, execute_or_add_commands +from antarest.core.utils.string import to_camel_case +from antarest.study.business.utils import AllOptionalMetaclass, camel_case_model, execute_or_add_commands from antarest.study.model import Study from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import BindingConstraintFrequency from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id @@ -178,7 +179,7 @@ def accept(self, constraint: "BindingConstraintConfigType") -> bool: return False # Filter on terms - terms = constraint.constraints or [] + terms = constraint.terms or [] if self.area_name: all_areas = [] @@ -219,21 +220,17 @@ def accept(self, constraint: "BindingConstraintConfigType") -> bool: return True -class BindingConstraintEditionModel(BaseModel, metaclass=AllOptionalMetaclass): - group: str - enabled: bool - time_step: BindingConstraintFrequency - operator: BindingConstraintOperator - filter_year_by_year: str - filter_synthesis: str - comments: str +@camel_case_model +class BindingConstraintEditionModel(BindingConstraintProperties870, metaclass=AllOptionalMetaclass, use_none=True): coeffs: Dict[str, List[float]] +@camel_case_model class BindingConstraintEdition(BindingConstraintMatrices, BindingConstraintEditionModel): pass +@camel_case_model class BindingConstraintCreation(BindingConstraintMatrices, BindingConstraintProperties870): name: str coeffs: Dict[str, List[float]] @@ -241,6 +238,10 @@ class BindingConstraintCreation(BindingConstraintMatrices, BindingConstraintProp # Ajout d'un root validator pour valider les dimensions des matrices @root_validator(pre=True) def check_matrices_dimensions(cls, values: Dict[str, Any]) -> Dict[str, Any]: + for _key in ["time_step", "less_term_matrix", "equal_term_matrix", "greater_term_matrix"]: + _camel = to_camel_case(_key) + values[_key] = values.pop(_camel, values.get(_key)) + # The dimensions of the matrices depend on the frequency and the version of the study. if values.get("time_step") is None: return values @@ -290,10 +291,18 @@ def check_matrices_dimensions(cls, values: Dict[str, Any]) -> Dict[str, Any]: raise ValueError(err_msg) -class BindingConstraintConfig(BindingConstraintProperties): +@camel_case_model +class _BindingConstraintConfig(BindingConstraintProperties): id: str name: str - constraints: Optional[List[ConstraintTermDTO]] + + +class BindingConstraintConfig(_BindingConstraintConfig): + terms: MutableSequence[ConstraintTermDTO] = Field( + default_factory=lambda: [], + alias="constraints", # only for backport compatibility + title="Constraint terms", + ) class BindingConstraintConfig870(BindingConstraintConfig): @@ -361,9 +370,9 @@ def parse_constraint(key: str, value: str, char: str, new_config: BindingConstra if len(weight_and_offset) == 2: weight = float(weight_and_offset[0]) offset = float(weight_and_offset[1]) - if new_config.constraints is None: - new_config.constraints = [] - new_config.constraints.append( + if new_config.terms is None: + new_config.terms = [] + new_config.terms.append( ConstraintTermDTO( id=key, weight=weight, @@ -387,18 +396,17 @@ def process_constraint(constraint_value: Dict[str, Any], version: int) -> Bindin args = { "id": constraint_value["id"], "name": constraint_value["name"], - "enabled": constraint_value["enabled"], - "time_step": constraint_value["type"], - "operator": constraint_value["operator"], - "comments": constraint_value.get("comments", None), + "enabled": constraint_value.get("enabled", True), + "time_step": constraint_value.get("type", BindingConstraintFrequency.HOURLY), + "operator": constraint_value.get("operator", BindingConstraintOperator.EQUAL), + "comments": constraint_value.get("comments", ""), "filter_year_by_year": constraint_value.get("filter-year-by-year", ""), "filter_synthesis": constraint_value.get("filter-synthesis", ""), - "constraints": None, } if version < 870: new_config: BindingConstraintConfigType = BindingConstraintConfig(**args) else: - args["group"] = constraint_value.get("group") + args["group"] = constraint_value.get("group", DEFAULT_GROUP) new_config = BindingConstraintConfig870(**args) for key, value in constraint_value.items(): @@ -413,8 +421,8 @@ def constraints_to_coeffs( constraint: BindingConstraintConfigType, ) -> Dict[str, List[float]]: coeffs: Dict[str, List[float]] = {} - if constraint.constraints is not None: - for term in constraint.constraints: + if constraint.terms is not None: + for term in constraint.terms: if term.id is not None and term.weight is not None: coeffs[term.id] = [term.weight] if term.offset is not None: @@ -640,7 +648,7 @@ def update_constraint_term( if not isinstance(constraint, BindingConstraintConfig) and not isinstance(constraint, BindingConstraintConfig): raise BindingConstraintNotFoundError(study.id) - constraint_terms = constraint.constraints # existing constraint terms + constraint_terms = constraint.terms # existing constraint terms if constraint_terms is None: raise NoConstraintError(study.id) @@ -695,11 +703,11 @@ def add_new_constraint_term( raise MissingDataError("Add new constraint term : data is missing") constraint_id = constraint_term.data.generate_id() - constraints_term = constraint.constraints or [] - if find_constraint_term_id(constraints_term, constraint_id) >= 0: + constraint_terms = constraint.terms or [] + if find_constraint_term_id(constraint_terms, constraint_id) >= 0: raise ConstraintAlreadyExistError(study.id) - constraints_term.append( + constraint_terms.append( ConstraintTermDTO( id=constraint_id, weight=constraint_term.weight if constraint_term.weight is not None else 0.0, @@ -708,7 +716,7 @@ def add_new_constraint_term( ) ) coeffs = {} - for term in constraints_term: + for term in constraint_terms: coeffs[term.id] = [term.weight] if term.offset is not None: coeffs[term.id].append(term.offset) @@ -759,7 +767,7 @@ def _replace_matrices_according_to_frequency_and_version( return args -def find_constraint_term_id(constraints_term: List[ConstraintTermDTO], constraint_term_id: str) -> int: +def find_constraint_term_id(constraints_term: Sequence[ConstraintTermDTO], constraint_term_id: str) -> int: try: index = [elm.id for elm in constraints_term].index(constraint_term_id) return index diff --git a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py index 4b3885a5f0..a8c04d657f 100644 --- a/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py +++ b/antarest/study/storage/variantstudy/model/command/create_binding_constraint.py @@ -71,13 +71,13 @@ def check_matrix_values(time_step: BindingConstraintFrequency, values: MatrixTyp raise ValueError("Matrix values cannot contain NaN") -class BindingConstraintProperties(BaseModel, extra=Extra.forbid): +class BindingConstraintProperties(BaseModel, extra=Extra.forbid, allow_population_by_field_name=True): enabled: bool = True - time_step: BindingConstraintFrequency - operator: BindingConstraintOperator - filter_year_by_year: t.Optional[str] = None - filter_synthesis: t.Optional[str] = None - comments: t.Optional[str] = None + time_step: BindingConstraintFrequency = BindingConstraintFrequency.HOURLY + operator: BindingConstraintOperator = BindingConstraintOperator.EQUAL + comments: str = "" + filter_year_by_year: str = "" + filter_synthesis: str = "" class BindingConstraintProperties870(BindingConstraintProperties): diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 2bcb56100f..c6aa7fe6e6 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -1,8 +1,10 @@ import enum import logging +import warnings from http import HTTPStatus from typing import Any, Dict, List, Mapping, Optional, Sequence, Union, cast +import typing_extensions as te from fastapi import APIRouter, Body, Depends, Query from starlette.responses import RedirectResponse @@ -68,6 +70,13 @@ logger = logging.getLogger(__name__) +class BCKeyValueType(te.TypedDict): + """Deprecated type for binding constraint key-value pair (used for update)""" + + key: str + value: Union[str, int, float, bool] + + class ClusterType(str, enum.Enum): """ Cluster type: @@ -972,7 +981,7 @@ def get_binding_constraint( def update_binding_constraint( uuid: str, binding_constraint_id: str, - data: BindingConstraintEdition, + data: Union[BCKeyValueType, BindingConstraintEdition], current_user: JWTUser = Depends(auth.get_current_user), ) -> BindingConstraintConfigType: logger.info( @@ -981,6 +990,18 @@ def update_binding_constraint( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) + + if isinstance(data, dict): + warnings.warn( + "Using key / value format for binding constraint data is deprecated." + " Please use the BindingConstraintEdition format instead.", + DeprecationWarning, + ) + _obj = {data["key"]: data["value"]} + if "filterByYear" in _obj: + _obj["filterYearByYear"] = _obj.pop("filterByYear") + data = BindingConstraintEdition(**_obj) + return study_service.binding_constraint_manager.update_binding_constraint(study, binding_constraint_id, data) @bp.get( diff --git a/tests/integration/study_data_blueprint/test_binding_constraints.py b/tests/integration/study_data_blueprint/test_binding_constraints.py index 5950a8b121..3cf4a921cd 100644 --- a/tests/integration/study_data_blueprint/test_binding_constraints.py +++ b/tests/integration/study_data_blueprint/test_binding_constraints.py @@ -202,7 +202,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st json={ "name": "binding_constraint_3", "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "New API", @@ -222,36 +222,36 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st expected = [ { "comments": "", - "constraints": None, + "constraints": [], # should be renamed to `terms` in the future. "enabled": True, - "filter_synthesis": "", - "filter_year_by_year": "", + "filterSynthesis": "", + "filterYearByYear": "", "id": "binding_constraint_1", "name": "binding_constraint_1", "operator": "less", - "time_step": "hourly", + "timeStep": "hourly", }, { "comments": "", - "constraints": None, + "constraints": [], # should be renamed to `terms` in the future. "enabled": True, - "filter_synthesis": "", - "filter_year_by_year": "", + "filterSynthesis": "", + "filterYearByYear": "", "id": "binding_constraint_2", "name": "binding_constraint_2", "operator": "less", - "time_step": "hourly", + "timeStep": "hourly", }, { "comments": "New API", - "constraints": None, + "constraints": [], # should be renamed to `terms` in the future. "enabled": True, - "filter_synthesis": "", - "filter_year_by_year": "", + "filterSynthesis": "", + "filterYearByYear": "", "id": "binding_constraint_3", "name": "binding_constraint_3", "operator": "less", - "time_step": "hourly", + "timeStep": "hourly", }, ] assert binding_constraints_list == expected @@ -301,7 +301,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st ) assert res.status_code == 200, res.json() binding_constraint = res.json() - constraint_terms = binding_constraint["constraints"] + constraint_terms = binding_constraint["constraints"] # should be renamed to `terms` in the future. expected = [ { "data": {"area1": area1_id, "area2": area2_id}, @@ -336,7 +336,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st ) assert res.status_code == 200, res.json() binding_constraint = res.json() - constraint_terms = binding_constraint["constraints"] + constraint_terms = binding_constraint["constraints"] # should be renamed to `terms` in the future. expected = [ { "data": {"area1": area1_id, "area2": area2_id}, @@ -405,17 +405,17 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st assert res.status_code == 200 assert res.json()["comments"] == new_comment - # The user change the time_step to daily instead of hourly. + # The user change the timeStep to daily instead of hourly. # We must check that the matrix is a daily/weekly matrix. res = client.put( f"/v1/studies/{study_id}/bindingconstraints/{bc_id}", - json={"time_step": "daily"}, + json={"timeStep": "daily"}, headers=user_headers, ) - assert res.status_code == 200 - assert res.json()["time_step"] == "daily" + assert res.status_code == 200, res.json() + assert res.json()["timeStep"] == "daily" - # Check the last command is a change time_step + # Check that the command corresponds to a change in `time_step` if study_type == "variant": res = client.get(f"/v1/studies/{study_id}/commands", headers=user_headers) commands = res.json() @@ -444,7 +444,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st json={ "name": " ", "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "New API", @@ -463,7 +463,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st json={ "name": "%%**", "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "New API", @@ -482,7 +482,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st json={ "name": bc_id, "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "", @@ -497,7 +497,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st json={ "name": "binding_constraint_x", "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "2 types of matrices", @@ -519,7 +519,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st json={ "name": "binding_constraint_x", "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "Incoherent matrix with version", @@ -536,7 +536,7 @@ def test_lifecycle__nominal(self, client: TestClient, user_access_token: str, st wrong_request_args = { "name": "binding_constraint_5", "enabled": True, - "time_step": "daily", + "timeStep": "daily", "operator": "less", "coeffs": {}, "comments": "Creation with matrix", @@ -616,7 +616,7 @@ def test_for_version_870(self, client: TestClient, admin_access_token: str, stud # Creation of a bc without group bc_id_wo_group = "binding_constraint_1" - args = {"enabled": True, "time_step": "hourly", "operator": "less", "coeffs": {}, "comments": "New API"} + args = {"enabled": True, "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "New API"} res = client.post( f"/v1/studies/{study_id}/bindingconstraints", json={"name": bc_id_wo_group, **args}, @@ -697,11 +697,11 @@ def test_for_version_870(self, client: TestClient, admin_access_token: str, stud assert res.status_code == 200 assert res.json()["data"] == matrix_lt3.tolist() - # The user changed the time_step to daily instead of hourly. + # The user changed the timeStep to daily instead of hourly. # We must check that the matrices have been updated. res = client.put( f"/v1/studies/{study_id}/bindingconstraints/{bc_id_w_matrix}", - json={"time_step": "daily"}, + json={"timeStep": "daily"}, headers=admin_headers, ) assert res.status_code == 200, res.json() @@ -757,7 +757,7 @@ def test_for_version_870(self, client: TestClient, admin_access_token: str, stud json={ "name": "binding_constraint_700", "enabled": True, - "time_step": "hourly", + "timeStep": "hourly", "operator": "less", "coeffs": {}, "comments": "New API", @@ -932,15 +932,15 @@ def test_for_version_870(self, client: TestClient, admin_access_token: str, stud assert groups["Group 2"] == [ { "comments": "New API", - "constraints": None, + "constraints": [], "enabled": True, - "filter_synthesis": "", - "filter_year_by_year": "", + "filterSynthesis": "", + "filterYearByYear": "", "group": "Group 2", "id": "second bc", "name": "Second BC", "operator": "less", - "time_step": "hourly", + "timeStep": "hourly", } ] diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 33b059ccae..c99b8c9e0e 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -1706,7 +1706,7 @@ def test_area_management(client: TestClient, admin_access_token: str, study_id: binding_constraint_1 = res.json() assert res.status_code == 200 - constraint = binding_constraint_1["constraints"][0] + constraint = binding_constraint_1["constraints"][0] # should be renamed to `terms` in the future. assert constraint["id"] == "area 1.cluster 1" assert constraint["weight"] == 2.0 assert constraint["offset"] == 4.0 diff --git a/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts b/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts index 89e368e793..df777fb9b7 100644 --- a/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts +++ b/webapp/src/components/App/Singlestudy/Commands/Edition/commandTypes.ts @@ -52,7 +52,7 @@ export enum BindingConstraintOperator { export interface CreateBindingConstraint { name: string; enabled: boolean; - time_step: TimeStep; + timeStep: TimeStep; operator: BindingConstraintOperator; coeffs: Record; values?: MatrixData[][] | string; @@ -117,7 +117,7 @@ export interface ReplaceMatrix { export interface UpdateBindingConstraint { id: string; enabled: boolean; - time_step: TimeStep; + timeStep: TimeStep; operator: BindingConstraintOperator; coeffs: Record; values?: MatrixData[][] | string; diff --git a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/AddDialog.tsx b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/AddDialog.tsx index 21e174ff96..64f379ff6b 100644 --- a/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/AddDialog.tsx +++ b/webapp/src/components/App/Singlestudy/explore/Modelization/BindingConstraints/AddDialog.tsx @@ -30,7 +30,7 @@ function AddDialog({ studyId, existingConstraints, open, onClose }: Props) { const defaultValues = { name: "", enabled: true, - time_step: TimeStep.HOURLY, + timeStep: TimeStep.HOURLY, operator: BindingConstraintOperator.LESS, comments: "", coeffs: {}, @@ -113,7 +113,7 @@ function AddDialog({ studyId, existingConstraints, open, onClose }: Props) { control={control} /> ; + timeStep: Exclude; operator: OperatorType; comments?: string; filterByYear: FilteringType[]; @@ -37,11 +37,11 @@ export interface BindingConstFieldsDTO { name: string; id: string; enabled: boolean; - time_step: Exclude; + timeStep: Exclude; operator: OperatorType; comments?: string; - filter_year_by_year?: string; - filter_synthesis?: string; + filterYearByYear?: string; + filterSynthesis?: string; constraints: ConstraintType[]; } @@ -59,11 +59,11 @@ export async function getDefaultValues( return { ...fields, comments: fields.comments || "", - filterByYear: (fields.filter_year_by_year || "").split(",").map((elm) => { + filterByYear: (fields.filterYearByYear || "").split(",").map((elm) => { const sElm = elm.replace(/\s+/g, ""); return sElm as FilteringType; }), - filterSynthesis: (fields.filter_synthesis || "").split(",").map((elm) => { + filterSynthesis: (fields.filterSynthesis || "").split(",").map((elm) => { const sElm = elm.replace(/\s+/g, ""); return sElm as FilteringType; }),