Skip to content

Commit

Permalink
feat(delete-apis): not allow deletion for areas, links and thermals w…
Browse files Browse the repository at this point in the history
…hen referenced in a bc
  • Loading branch information
mabw-rte committed Jun 18, 2024
1 parent da4ac01 commit 92a558d
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 2 deletions.
33 changes: 33 additions & 0 deletions antarest/core/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,39 @@ def __init__(self, is_variant: bool) -> None:
super().__init__(HTTPStatus.EXPECTATION_FAILED, "Upgrade not supported for parent of variants")


class AreaDeletionNotAllowed(HTTPException):
def __init__(self, uuid: str, message: t.Optional[str] = None) -> None:
msg = f"Area {uuid} is not allowed to be deleted"
if message:
msg += f"\n{message}"
super().__init__(
HTTPStatus.FORBIDDEN,
msg,
)


class LinkDeletionNotAllowed(HTTPException):
def __init__(self, uuid: str, message: t.Optional[str] = None) -> None:
msg = f"Link {uuid} is not allowed to be deleted"
if message:
msg += f"\n{message}"
super().__init__(
HTTPStatus.FORBIDDEN,
msg,
)


class ClusterDeletionNotAllowed(HTTPException):
def __init__(self, uuid: str, message: t.Optional[str] = None) -> None:
msg = f"Cluster {uuid} is not allowed to be deleted"
if message:
msg += f"\n{message}"
super().__init__(
HTTPStatus.FORBIDDEN,
msg,
)


class UnsupportedStudyVersion(HTTPException):
def __init__(self, version: str) -> None:
super().__init__(
Expand Down
51 changes: 50 additions & 1 deletion antarest/study/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@

from antarest.core.config import Config
from antarest.core.exceptions import (
AreaDeletionNotAllowed,
BadEditInstructionException,
ClusterDeletionNotAllowed,
CommandApplicationError,
IncorrectPathError,
LinkDeletionNotAllowed,
NotAManagedStudyException,
StudyDeletionNotAllowed,
StudyNotFoundError,
Expand Down Expand Up @@ -56,7 +59,7 @@
from antarest.study.business.areas.renewable_management import RenewableManager
from antarest.study.business.areas.st_storage_management import STStorageManager
from antarest.study.business.areas.thermal_management import ThermalManager
from antarest.study.business.binding_constraint_management import BindingConstraintManager
from antarest.study.business.binding_constraint_management import BindingConstraintManager, ConstraintFilters, LinkTerm
from antarest.study.business.config_management import ConfigManager
from antarest.study.business.correlation_management import CorrelationManager
from antarest.study.business.district_manager import DistrictManager
Expand Down Expand Up @@ -137,6 +140,7 @@
logger = logging.getLogger(__name__)

MAX_MISSING_STUDY_TIMEOUT = 2 # days
MAX_BINDING_CONSTRAINTS_TO_DISPLAY = 10


def get_disk_usage(path: t.Union[str, Path]) -> int:
Expand Down Expand Up @@ -1858,6 +1862,16 @@ def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> Non
study = self.get_study(uuid)
assert_permission(params.user, study, StudyPermissionType.WRITE)
self._assert_study_unarchived(study)
referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints(
study, ConstraintFilters(area_name=area_id)
)[:MAX_BINDING_CONSTRAINTS_TO_DISPLAY]
if referencing_binding_constraints:
first_bcs_ids = ""
for i, bc in enumerate(referencing_binding_constraints):
first_bcs_ids += f"{i+1}- {bc.id}\n"
raise AreaDeletionNotAllowed(
area_id, "Area is referenced in the following binding constraints:\n" + first_bcs_ids
)
self.areas.delete_area(study, area_id)
self.event_bus.push(
Event(
Expand All @@ -1877,6 +1891,17 @@ def delete_link(
study = self.get_study(uuid)
assert_permission(params.user, study, StudyPermissionType.WRITE)
self._assert_study_unarchived(study)
link_id = LinkTerm(area1=area_from, area2=area_to).generate_id()
referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints(
study, ConstraintFilters(link_id=link_id)
)[:MAX_BINDING_CONSTRAINTS_TO_DISPLAY]
if referencing_binding_constraints:
first_bcs_ids = ""
for i, bc in enumerate(referencing_binding_constraints):
first_bcs_ids += f"{i+1}- {bc.id}\n"
raise LinkDeletionNotAllowed(
link_id, "Link is referenced in the following binding constraints:\n" + first_bcs_ids
)
self.links.delete_link(study, area_from, area_to)
self.event_bus.push(
Event(
Expand Down Expand Up @@ -2518,3 +2543,27 @@ def get_matrix_with_index_and_header(
)

return df_matrix

def assert_no_cluster_referenced_in_bcs(self, study: Study, cluster_ids: t.Sequence[str]) -> None:
"""
Check that no cluster is referenced in a binding constraint otherwise raise an ClusterDeletionNotAllowed Exception
Args:
study: input study
cluster_ids: cluster IDs to be checked
Returns:
"""

for cluster_id in cluster_ids:
referencing_binding_constraints = self.binding_constraint_manager.get_binding_constraints(
study, ConstraintFilters(cluster_id=cluster_id)
)[:MAX_BINDING_CONSTRAINTS_TO_DISPLAY]
if referencing_binding_constraints:
first_bcs_ids = ""
for i, bc in enumerate(referencing_binding_constraints):
first_bcs_ids += f"{i+1}- {bc.id}\n"
raise ClusterDeletionNotAllowed(
cluster_id, "Cluster is referenced in the following binding constraints:\n" + first_bcs_ids
)
1 change: 1 addition & 0 deletions antarest/study/web/study_data_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,7 @@ def delete_thermal_clusters(
)
request_params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, request_params)
study_service.assert_no_cluster_referenced_in_bcs(study, cluster_ids)
study_service.thermal_manager.delete_clusters(study, area_id, cluster_ids)

@bp.get(
Expand Down
20 changes: 20 additions & 0 deletions tests/integration/study_data_blueprint/test_thermal.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from antarest.core.utils.string import to_camel_case
from antarest.study.storage.rawstudy.model.filesystem.config.model import transform_name_to_id
from antarest.study.storage.rawstudy.model.filesystem.config.thermal import ThermalProperties
from antarest.study.storage.variantstudy.model.command.common import CommandName
from tests.integration.utils import wait_task_completion

DEFAULT_PROPERTIES = json.loads(ThermalProperties(name="Dummy").json())
Expand Down Expand Up @@ -1003,6 +1004,25 @@ def test_variant_lifecycle(self, client: TestClient, user_access_token: str, var
assert res.status_code == 200
assert res.json()["data"] == matrix

# # create a binding constraint for
# res = client.post(
# f"/v1/studies/{variant_id}/commands",
# headers={"Authorization": f"Bearer {user_access_token}"},
# json=[
# {
# "action": CommandName.CREATE_BINDING_CONSTRAINT.value,
# "args": {
# "name": "binding constraint 1",
# "enabled": True,
# "time_step": "hourly",
# "operator": "less",
# "coeffs": {f"{area_id}.{cluster_id}": [2.0, 4]},
# },
# }
# ],
# )
# res.raise_for_status()

# Delete the thermal cluster
res = client.delete(
f"/v1/studies/{variant_id}/areas/{area_id}/clusters/thermal",
Expand Down
16 changes: 15 additions & 1 deletion tests/integration/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1427,8 +1427,22 @@ def test_area_management(client: TestClient, admin_access_token: str) -> None:
},
}

# check that at this stage the area cannot be deleted as it is referenced in binding constraint 1
result = client.delete(f"/v1/studies/{study_id}/areas/area%201")
assert result.status_code == 200
assert result.status_code == 403, res.json()
# verify the error message
assert (
result.json()["description"] == "Area area 1 is not allowed to be deleted\nArea is referenced "
"in the following binding constraints:\n1- binding constraint 1\n"
)
# check the exception
assert result.json()["exception"] == "AreaDeletionNotAllowed"

# delete binding constraint 1
result = client.delete(f"/v1/studies/{study_id}/bindingconstraints/binding%20constraint%201")
# check now that we can delete the area 1
result = client.delete(f"/v1/studies/{study_id}/areas/area%201")
assert result.status_code == 200, res.json()
res_areas = client.get(f"/v1/studies/{study_id}/areas")
assert res_areas.json() == [
{
Expand Down

0 comments on commit 92a558d

Please sign in to comment.