Skip to content

Commit

Permalink
feat: add an endpoint to allow multiple deletion of binding constrain… (
Browse files Browse the repository at this point in the history
  • Loading branch information
TheoPascoli authored Jan 17, 2025
1 parent 7edcc46 commit d14454a
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 22 deletions.
40 changes: 40 additions & 0 deletions antarest/study/business/binding_constraint_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
)
from antarest.study.storage.variantstudy.model.command.icommand import ICommand
from antarest.study.storage.variantstudy.model.command.remove_binding_constraint import RemoveBindingConstraint
from antarest.study.storage.variantstudy.model.command.remove_multiple_binding_constraints import (
RemoveMultipleBindingConstraints,
)
from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix
from antarest.study.storage.variantstudy.model.command.update_binding_constraint import (
UpdateBindingConstraint,
Expand Down Expand Up @@ -589,6 +592,18 @@ def terms_to_coeffs(terms: t.Sequence[ConstraintTerm]) -> t.Dict[str, t.List[flo
coeffs[term.id].append(term.offset)
return coeffs

def check_binding_constraints_exists(self, study: Study, bc_ids: t.List[str]) -> None:
storage_service = self.storage_service.get_storage(study)
file_study = storage_service.get_raw(study)
existing_constraints = file_study.tree.get(["input", "bindingconstraints", "bindingconstraints"])

existing_ids = {constraint["id"] for constraint in existing_constraints.values()}

missing_bc_ids = [bc_id for bc_id in bc_ids if bc_id not in existing_ids]

if missing_bc_ids:
raise BindingConstraintNotFound(f"Binding constraint(s) '{missing_bc_ids}' not found")

def get_binding_constraint(self, study: Study, bc_id: str) -> ConstraintOutput:
"""
Retrieves a binding constraint by its ID within a given study.
Expand Down Expand Up @@ -1018,6 +1033,31 @@ def remove_binding_constraint(self, study: Study, binding_constraint_id: str) ->
)
execute_or_add_commands(study, file_study, [command], self.storage_service)

def remove_multiple_binding_constraints(self, study: Study, binding_constraints_ids: t.List[str]) -> None:
"""
Removes multiple binding constraints from a study.
Args:
study: The study from which to remove the constraint.
binding_constraints_ids: The IDs of the binding constraints to remove.
Raises:
BindingConstraintNotFound: If at least one binding constraint within the specified list is not found.
"""

self.check_binding_constraints_exists(study, binding_constraints_ids)

command_context = self.storage_service.variant_study_service.command_factory.command_context
file_study = self.storage_service.get_storage(study).get_raw(study)

command = RemoveMultipleBindingConstraints(
ids=binding_constraints_ids,
command_context=command_context,
study_version=file_study.config.version,
)

execute_or_add_commands(study, file_study, [command], self.storage_service)

def _update_constraint_with_terms(
self, study: Study, bc: ConstraintOutput, terms: t.Mapping[str, ConstraintTerm]
) -> None:
Expand Down
11 changes: 11 additions & 0 deletions antarest/study/storage/variantstudy/business/command_reverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster
from antarest.study.storage.variantstudy.model.command.remove_district import RemoveDistrict
from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink
from antarest.study.storage.variantstudy.model.command.remove_multiple_binding_constraints import (
RemoveMultipleBindingConstraints,
)
from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster
from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage
from antarest.study.storage.variantstudy.model.command.remove_user_resource import RemoveUserResource
Expand Down Expand Up @@ -163,6 +166,14 @@ def _revert_remove_binding_constraint(
) -> t.List[ICommand]:
raise NotImplementedError("The revert function for RemoveBindingConstraint is not available")

@staticmethod
def _revert_remove_multiple_binding_constraints(
base_command: RemoveMultipleBindingConstraints,
history: t.List["ICommand"],
base: FileStudy,
) -> t.List[ICommand]:
raise NotImplementedError("The revert function for RemoveMultipleBindingConstraints is not available")

@staticmethod
def _revert_update_scenario_builder(
base_command: UpdateScenarioBuilder,
Expand Down
4 changes: 4 additions & 0 deletions antarest/study/storage/variantstudy/command_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster
from antarest.study.storage.variantstudy.model.command.remove_district import RemoveDistrict
from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink
from antarest.study.storage.variantstudy.model.command.remove_multiple_binding_constraints import (
RemoveMultipleBindingConstraints,
)
from antarest.study.storage.variantstudy.model.command.remove_renewables_cluster import RemoveRenewablesCluster
from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage
from antarest.study.storage.variantstudy.model.command.remove_user_resource import RemoveUserResource
Expand All @@ -63,6 +66,7 @@
CommandName.CREATE_BINDING_CONSTRAINT.value: CreateBindingConstraint,
CommandName.UPDATE_BINDING_CONSTRAINT.value: UpdateBindingConstraint,
CommandName.REMOVE_BINDING_CONSTRAINT.value: RemoveBindingConstraint,
CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value: RemoveMultipleBindingConstraints,
CommandName.CREATE_THERMAL_CLUSTER.value: CreateCluster,
CommandName.REMOVE_THERMAL_CLUSTER.value: RemoveCluster,
CommandName.CREATE_RENEWABLES_CLUSTER.value: CreateRenewablesCluster,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Copyright (c) 2025, RTE (https://www.rte-france.com)
#
# See AUTHORS.txt
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.
import typing as t

from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy


def remove_bc_from_scenario_builder(study_data: FileStudy, removed_groups: t.Set[str]) -> None:
"""
Update the scenario builder by removing the rows that correspond to the BC groups to remove.
NOTE: this update can be very long if the scenario builder configuration is large.
"""
if not removed_groups:
return

rulesets = study_data.tree.get(["settings", "scenariobuilder"])

for ruleset in rulesets.values():
for key in list(ruleset):
# The key is in the form "symbol,group,year"
symbol, *parts = key.split(",")
if symbol == "bc" and parts[0] in removed_groups:
del ruleset[key]

study_data.tree.save(rulesets, ["settings", "scenariobuilder"])
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class CommandName(Enum):
CREATE_BINDING_CONSTRAINT = "create_binding_constraint"
UPDATE_BINDING_CONSTRAINT = "update_binding_constraint"
REMOVE_BINDING_CONSTRAINT = "remove_binding_constraint"
REMOVE_MULTIPLE_BINDING_CONSTRAINTS = "remove_multiple_binding_constraints"
CREATE_THERMAL_CLUSTER = "create_cluster"
REMOVE_THERMAL_CLUSTER = "remove_cluster"
CREATE_RENEWABLES_CLUSTER = "create_renewables_cluster"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from antarest.study.storage.variantstudy.business.utils_binding_constraint import (
parse_bindings_coeffs_and_save_into_config,
)
from antarest.study.storage.variantstudy.model.command.binding_constraint_utils import remove_bc_from_scenario_builder
from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput
from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand
from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener
Expand Down Expand Up @@ -503,24 +504,3 @@ def match(self, other: "ICommand", equal: bool = False) -> bool:
if not equal:
return self.name == other.name
return super().match(other, equal)


def remove_bc_from_scenario_builder(study_data: FileStudy, removed_groups: t.Set[str]) -> None:
"""
Update the scenario builder by removing the rows that correspond to the BC groups to remove.
NOTE: this update can be very long if the scenario builder configuration is large.
"""
if not removed_groups:
return

rulesets = study_data.tree.get(["settings", "scenariobuilder"])

for ruleset in rulesets.values():
for key in list(ruleset):
# The key is in the form "symbol,group,year"
symbol, *parts = key.split(",")
if symbol == "bc" and parts[0] in removed_groups:
del ruleset[key]

study_data.tree.save(rulesets, ["settings", "scenariobuilder"])
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import DEFAULT_GROUP
from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.model.command.binding_constraint_utils import remove_bc_from_scenario_builder
from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput
from antarest.study.storage.variantstudy.model.command.create_binding_constraint import remove_bc_from_scenario_builder
from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand
from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener
from antarest.study.storage.variantstudy.model.model import CommandDTO
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright (c) 2025, RTE (https://www.rte-france.com)
#
# See AUTHORS.txt
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.
import typing as t

from typing_extensions import override

from antarest.study.model import STUDY_VERSION_8_7
from antarest.study.storage.rawstudy.model.filesystem.config.binding_constraint import DEFAULT_GROUP
from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy
from antarest.study.storage.variantstudy.model.command.binding_constraint_utils import remove_bc_from_scenario_builder
from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput
from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand, OutputTuple
from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener
from antarest.study.storage.variantstudy.model.model import CommandDTO


class RemoveMultipleBindingConstraints(ICommand):
"""
Command used to remove multiple binding constraints at once.
"""

command_name: CommandName = CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS
version: int = 1

# Properties of the `REMOVE_MULTIPLE_BINDING_CONSTRAINTS` command:
ids: t.List[str]

@override
def _apply_config(self, study_data: FileStudyTreeConfig) -> OutputTuple:
# If at least one bc is missing in the database, we raise an error
already_existing_ids = {binding.id for binding in study_data.bindings}
missing_bc_ids = [id_ for id_ in self.ids if id_ not in already_existing_ids]
if missing_bc_ids:
return CommandOutput(status=False, message=f"Binding constraint not found: '{missing_bc_ids}'"), {}
return CommandOutput(status=True), {}

@override
def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput:
command_output, _ = self._apply_config(study_data.config)

if not command_output.status:
return command_output

binding_constraints = study_data.tree.get(["input", "bindingconstraints", "bindingconstraints"])

old_groups = {bd.get("group", DEFAULT_GROUP).lower() for bd in binding_constraints.values()}

deleted_binding_constraints = []

for key in list(binding_constraints.keys()):
if binding_constraints[key].get("id") in self.ids:
deleted_binding_constraints.append(binding_constraints.pop(key))

study_data.tree.save(
binding_constraints,
["input", "bindingconstraints", "bindingconstraints"],
)

existing_files = study_data.tree.get(["input", "bindingconstraints"], depth=1)
for bc in deleted_binding_constraints:
if study_data.config.version < STUDY_VERSION_8_7:
study_data.tree.delete(["input", "bindingconstraints", bc.get("id")])
else:
for term in ["lt", "gt", "eq"]:
matrix_id = f"{bc.get('id')}_{term}"
if matrix_id in existing_files:
study_data.tree.delete(["input", "bindingconstraints", matrix_id])

new_groups = {bd.get("group", DEFAULT_GROUP).lower() for bd in binding_constraints.values()}
removed_groups = old_groups - new_groups
remove_bc_from_scenario_builder(study_data, removed_groups)

return command_output

@override
def to_dto(self) -> CommandDTO:
return CommandDTO(
action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value,
args={
"ids": self.ids,
},
study_version=self.study_version,
)

@override
def match_signature(self) -> str:
return str(self.command_name.value + MATCH_SIGNATURE_SEPARATOR + ",".join(self.ids))

@override
def match(self, other: ICommand, equal: bool = False) -> bool:
if not isinstance(other, RemoveMultipleBindingConstraints):
return False
return self.ids == other.ids

@override
def _create_diff(self, other: "ICommand") -> t.List["ICommand"]:
return []

@override
def get_inner_matrices(self) -> t.List[str]:
return []
19 changes: 19 additions & 0 deletions antarest/study/web/study_data_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,25 @@ def delete_binding_constraint(
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params)
return study_service.binding_constraint_manager.remove_binding_constraint(study, binding_constraint_id)

@bp.delete(
"/studies/{uuid}/bindingconstraints",
tags=[APITag.study_data],
summary="Delete multiple binding constraints",
response_model=None,
)
def delete_multiple_binding_constraints(
uuid: str, binding_constraints_ids: t.List[str], current_user: JWTUser = Depends(auth.get_current_user)
) -> None:
logger.info(
f"Deleting the binding constraints {binding_constraints_ids!r} for study {uuid}",
extra={"user": current_user.id},
)
params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params)
return study_service.binding_constraint_manager.remove_multiple_binding_constraints(
study, binding_constraints_ids
)

@bp.post(
"/studies/{uuid}/bindingconstraints/{binding_constraint_id}/term",
tags=[APITag.study_data],
Expand Down
36 changes: 36 additions & 0 deletions tests/integration/study_data_blueprint/test_binding_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,26 @@ def test_for_version_870(self, client: TestClient, user_access_token: str, study
binding_constraints_list = preparer.get_binding_constraints(study_id)
assert len(binding_constraints_list) == 2

# Delete multiple binding constraint
preparer.create_binding_constraint(study_id, name="bc1", group="grp1", **args)
preparer.create_binding_constraint(study_id, name="bc2", group="grp2", **args)

binding_constraints_list = preparer.get_binding_constraints(study_id)
assert len(binding_constraints_list) == 4

res = client.request(
"DELETE",
f"/v1/studies/{study_id}/bindingconstraints",
json=["bc1", "bc2"],
)
assert res.status_code == 200, res.json()

# Asserts that the deletion worked
binding_constraints_list = preparer.get_binding_constraints(study_id)
assert len(binding_constraints_list) == 2
actual_ids = [constraint["id"] for constraint in binding_constraints_list]
assert actual_ids == ["binding_constraint_1", "binding_constraint_3"]

# =============================
# CONSTRAINT DUPLICATION
# =============================
Expand Down Expand Up @@ -1015,6 +1035,22 @@ def test_for_version_870(self, client: TestClient, user_access_token: str, study
# ERRORS
# =============================

# Deletion multiple binding constraints, one does not exist. Make sure none is deleted

binding_constraints_list = preparer.get_binding_constraints(study_id)
assert len(binding_constraints_list) == 3

res = client.request(
"DELETE",
f"/v1/studies/{study_id}/bindingconstraints",
json=["binding_constraint_1", "binding_constraint_2", "binding_constraint_3"],
)
assert res.status_code == 404, res.json()
assert res.json()["description"] == "Binding constraint(s) '['binding_constraint_2']' not found"

binding_constraints_list = preparer.get_binding_constraints(study_id)
assert len(binding_constraints_list) == 3

# Creation with wrong matrix according to version
for operator in ["less", "equal", "greater", "both"]:
args["operator"] = operator
Expand Down
10 changes: 10 additions & 0 deletions tests/variantstudy/test_command_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@
CommandDTO(
action=CommandName.REMOVE_BINDING_CONSTRAINT.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8
),
CommandDTO(
action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value,
args={"ids": ["id"]},
study_version=STUDY_VERSION_8_8,
),
CommandDTO(
action=CommandName.REMOVE_MULTIPLE_BINDING_CONSTRAINTS.value,
args=[{"ids": ["id"]}],
study_version=STUDY_VERSION_8_8,
),
CommandDTO(
action=CommandName.CREATE_THERMAL_CLUSTER.value,
args={
Expand Down

0 comments on commit d14454a

Please sign in to comment.