From 4b496b1983967cbd95ecc4aa4deecd7dbf2cf121 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE Date: Thu, 18 Jan 2024 14:04:33 +0100 Subject: [PATCH] refactor(api): refactor and rename the exceptions related to `.ini` file configuration --- antarest/core/exceptions.py | 247 ++++++++++++++---- .../business/areas/renewable_management.py | 18 +- .../business/areas/st_storage_management.py | 105 ++++---- .../business/areas/thermal_management.py | 16 +- .../study_data_blueprint/test_renewable.py | 6 +- .../study_data_blueprint/test_st_storage.py | 12 +- .../study_data_blueprint/test_thermal.py | 6 +- .../areas/test_st_storage_management.py | 51 ++-- 8 files changed, 292 insertions(+), 169 deletions(-) diff --git a/antarest/core/exceptions.py b/antarest/core/exceptions.py index dd9cd45a5e..079263ae91 100644 --- a/antarest/core/exceptions.py +++ b/antarest/core/exceptions.py @@ -1,3 +1,4 @@ +import re from http import HTTPStatus from typing import Optional @@ -8,64 +9,227 @@ class ShouldNotHappenException(Exception): pass -class STStorageFieldsNotFoundError(HTTPException): - """Fields of the short-term storage are not found""" +# ============================================================ +# Exceptions related to the study configuration (`.ini` files) +# ============================================================ - def __init__(self, storage_id: str) -> None: - detail = f"Fields of storage '{storage_id}' not found" - super().__init__(HTTPStatus.NOT_FOUND, detail) +# Naming convention for exceptions related to the study configuration: +# +# | Topic | NotFound (404) | Duplicate (409) | Invalid (422) | +# |---------------|-----------------------|------------------------|----------------------| +# | ConfigFile | ConfigFileNotFound | N/A | InvalidConfigFile | +# | ConfigSection | ConfigSectionNotFound | DuplicateConfigSection | InvalidConfigSection | +# | ConfigOption | ConfigOptionNotFound | DuplicateConfigOption | InvalidConfigOption | +# | Matrix | MatrixNotFound | DuplicateMatrix | InvalidMatrix | - def __str__(self) -> str: - return self.detail +THERMAL_CLUSTER = "thermal cluster" +RENEWABLE_CLUSTER = "renewable cluster" +SHORT_TERM_STORAGE = "short-term storage" + +# ============================================================ +# NotFound (404) +# ============================================================ + +_match_input_path = re.compile(r"input(?:/[\w*-]+)+").fullmatch -class STStorageMatrixNotFoundError(HTTPException): - """Matrix of the short-term storage is not found""" - def __init__(self, study_id: str, area_id: str, storage_id: str, ts_name: str) -> None: - detail = f"Time series '{ts_name}' of storage '{storage_id}' not found" +class ConfigFileNotFound(HTTPException): + """ + Exception raised when a configuration file is not found (404 Not Found). + + Notes: + The study ID is not provided because it is implicit. + + Attributes: + path: Path of the missing file(s) relative to the study directory. + area_ids: Sequence of area IDs for which the file(s) is/are missing. + """ + + object_name = "" + """Name of the object that is not found: thermal, renewables, etc.""" + + def __init__(self, path: str, *area_ids: str): + assert _match_input_path(path), f"Invalid path: '{path}'" + self.path = path + self.area_ids = area_ids + ids = ", ".join(f"'{a}'" for a in area_ids) + detail = { + 0: f"Path '{path}' not found", + 1: f"Path '{path}' not found for area {ids}", + 2: f"Path '{path}' not found for areas {ids}", + }[min(len(area_ids), 2)] + if self.object_name: + detail = f"{self.object_name.title()} {detail}" super().__init__(HTTPStatus.NOT_FOUND, detail) def __str__(self) -> str: + """Return a string representation of the exception.""" return self.detail -class STStorageConfigNotFoundError(HTTPException): - """Configuration for short-term storage is not found""" +class ThermalClusterConfigNotFound(ConfigFileNotFound): + """Configuration for thermal cluster is not found (404 Not Found)""" + + object_name = THERMAL_CLUSTER + + +class RenewableClusterConfigNotFound(ConfigFileNotFound): + """Configuration for renewable cluster is not found (404 Not Found)""" + + object_name = RENEWABLE_CLUSTER + + +class STStorageConfigNotFound(ConfigFileNotFound): + """Configuration for short-term storage is not found (404 Not Found)""" + + object_name = SHORT_TERM_STORAGE + + +class ConfigSectionNotFound(HTTPException): + """ + Exception raised when a configuration section is not found (404 Not Found). - def __init__(self, study_id: str, area_id: str) -> None: - detail = f"The short-term storage configuration of area '{area_id}' not found" + Notes: + The study ID is not provided because it is implicit. + + Attributes: + path: Path of the missing file(s) relative to the study directory. + section_id: ID of the missing section. + """ + + object_name = "" + """Name of the object that is not found: thermal, renewables, etc.""" + + def __init__(self, path: str, section_id: str): + assert _match_input_path(path), f"Invalid path: '{path}'" + self.path = path + self.section_id = section_id + object_name = self.object_name or "section" + detail = f"{object_name.title()} '{section_id}' not found in '{path}'" super().__init__(HTTPStatus.NOT_FOUND, detail) def __str__(self) -> str: + """Return a string representation of the exception.""" return self.detail -class STStorageNotFoundError(HTTPException): - """Short-term storage is not found""" +class ThermalClusterNotFound(ConfigSectionNotFound): + """Thermal cluster is not found (404 Not Found)""" + + object_name = THERMAL_CLUSTER + + +class RenewableClusterNotFound(ConfigSectionNotFound): + """Renewable cluster is not found (404 Not Found)""" + + object_name = RENEWABLE_CLUSTER + + +class STStorageNotFound(ConfigSectionNotFound): + """Short-term storage is not found (404 Not Found)""" + + object_name = SHORT_TERM_STORAGE - def __init__(self, study_id: str, area_id: str, st_storage_id: str) -> None: - detail = f"Short-term storage '{st_storage_id}' not found in area '{area_id}'" + +class MatrixNotFound(HTTPException): + """ + Exception raised when a matrix is not found (404 Not Found). + + Notes: + The study ID is not provided because it is implicit. + + Attributes: + path: Path of the missing file(s) relative to the study directory. + """ + + object_name = "" + """Name of the object that is not found: thermal, renewables, etc.""" + + def __init__(self, path: str): + assert _match_input_path(path), f"Invalid path: '{path}'" + self.path = path + detail = f"Matrix '{path}' not found" + if self.object_name: + detail = f"{self.object_name.title()} {detail}" super().__init__(HTTPStatus.NOT_FOUND, detail) def __str__(self) -> str: return self.detail -class DuplicateSTStorageId(HTTPException): - """Exception raised when trying to create a short-term storage with an already existing id.""" +class ThermalClusterMatrixNotFound(MatrixNotFound): + """Matrix of the thermal cluster is not found (404 Not Found)""" + + object_name = THERMAL_CLUSTER + + +class RenewableClusterMatrixNotFound(MatrixNotFound): + """Matrix of the renewable cluster is not found (404 Not Found)""" + + object_name = RENEWABLE_CLUSTER - def __init__(self, study_id: str, area_id: str, st_storage_id: str) -> None: - detail = f"Short term storage '{st_storage_id}' already exists in area '{area_id}'" + +class STStorageMatrixNotFound(MatrixNotFound): + """Matrix of the short-term storage is not found (404 Not Found)""" + + object_name = SHORT_TERM_STORAGE + + +# ============================================================ +# Duplicate (409) +# ============================================================ + + +class DuplicateConfigSection(HTTPException): + """ + Exception raised when a configuration section is duplicated (409 Conflict). + + Notes: + The study ID is not provided because it is implicit. + + Attributes: + area_id: ID of the area in which the section is duplicated. + duplicates: Sequence of duplicated IDs. + """ + + object_name = "" + """Name of the object that is duplicated: thermal, renewables, etc.""" + + def __init__(self, area_id: str, *duplicates: str): + self.area_id = area_id + self.duplicates = duplicates + ids = ", ".join(f"'{a}'" for a in duplicates) + detail = { + 0: f"Duplicates found in '{area_id}'", + 1: f"Duplicate found in '{area_id}': {ids}", + 2: f"Duplicates found in '{area_id}': {ids}", + }[min(len(duplicates), 2)] + if self.object_name: + detail = f"{self.object_name.title()} {detail}" super().__init__(HTTPStatus.CONFLICT, detail) def __str__(self) -> str: + """Return a string representation of the exception.""" return self.detail -class UnknownModuleError(Exception): - def __init__(self, message: str) -> None: - super(UnknownModuleError, self).__init__(message) +class DuplicateThermalCluster(DuplicateConfigSection): + """Duplicate Thermal cluster (409 Conflict)""" + + object_name = THERMAL_CLUSTER + + +class DuplicateRenewableCluster(DuplicateConfigSection): + """Duplicate Renewable cluster (409 Conflict)""" + + object_name = RENEWABLE_CLUSTER + + +class DuplicateSTStorage(DuplicateConfigSection): + """Duplicate Short-term storage (409 Conflict)""" + + object_name = SHORT_TERM_STORAGE class StudyNotFoundError(HTTPException): @@ -108,11 +272,6 @@ def __init__(self, message: str) -> None: super().__init__(HTTPStatus.LOCKED, message) -class StudyAlreadyExistError(HTTPException): - def __init__(self, message: str) -> None: - super().__init__(HTTPStatus.CONFLICT, message) - - class StudyValidationError(HTTPException): def __init__(self, message: str) -> None: super().__init__(HTTPStatus.UNPROCESSABLE_ENTITY, message) @@ -328,29 +487,3 @@ def __init__(self) -> None: HTTPStatus.BAD_REQUEST, "You cannot scan the default internal workspace", ) - - -class ClusterNotFound(HTTPException): - def __init__(self, cluster_id: str) -> None: - super().__init__( - HTTPStatus.NOT_FOUND, - f"Cluster: '{cluster_id}' not found", - ) - - -class ClusterConfigNotFound(HTTPException): - def __init__(self, area_id: str) -> None: - super().__init__( - HTTPStatus.NOT_FOUND, - f"Cluster configuration for area: '{area_id}' not found", - ) - - -class ClusterAlreadyExists(HTTPException): - """Exception raised when attempting to create a cluster with an already existing ID.""" - - def __init__(self, cluster_type: str, cluster_id: str) -> None: - super().__init__( - HTTPStatus.CONFLICT, - f"{cluster_type} cluster with ID '{cluster_id}' already exists and could not be created.", - ) diff --git a/antarest/study/business/areas/renewable_management.py b/antarest/study/business/areas/renewable_management.py index c4152924bf..7858409f17 100644 --- a/antarest/study/business/areas/renewable_management.py +++ b/antarest/study/business/areas/renewable_management.py @@ -3,7 +3,7 @@ from pydantic import validator -from antarest.core.exceptions import ClusterAlreadyExists, ClusterConfigNotFound, ClusterNotFound +from antarest.core.exceptions import DuplicateRenewableCluster, RenewableClusterConfigNotFound, RenewableClusterNotFound from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.business.utils import AllOptionalMetaclass, camel_case_model, execute_or_add_commands from antarest.study.model import Study @@ -132,7 +132,7 @@ def get_clusters(self, study: Study, area_id: str) -> t.Sequence[RenewableCluste List of cluster output for all clusters. Raises: - ClusterConfigNotFound: If the clusters configuration for the specified area is not found. + RenewableClusterConfigNotFound: If the clusters configuration for the specified area is not found. """ file_study = self._get_file_study(study) path = _CLUSTERS_PATH.format(area_id=area_id) @@ -140,7 +140,7 @@ def get_clusters(self, study: Study, area_id: str) -> t.Sequence[RenewableCluste try: clusters = file_study.tree.get(path.split("/"), depth=3) except KeyError: - raise ClusterConfigNotFound(area_id) + raise RenewableClusterConfigNotFound(path, area_id) return [create_renewable_output(study.version, cluster_id, cluster) for cluster_id, cluster in clusters.items()] @@ -192,14 +192,14 @@ def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> RenewableC The cluster output representation. Raises: - ClusterNotFound: If the specified cluster is not found within the area. + RenewableClusterNotFound: If the specified cluster is not found within the area. """ file_study = self._get_file_study(study) path = _CLUSTER_PATH.format(area_id=area_id, cluster_id=cluster_id) try: cluster = file_study.tree.get(path.split("/"), depth=1) except KeyError: - raise ClusterNotFound(cluster_id) + raise RenewableClusterNotFound(path, cluster_id) return create_renewable_output(study.version, cluster_id, cluster) def update_cluster( @@ -222,7 +222,7 @@ def update_cluster( The updated cluster configuration. Raises: - ClusterNotFound: If the cluster to update is not found. + RenewableClusterNotFound: If the cluster to update is not found. """ study_version = study.version @@ -232,7 +232,7 @@ def update_cluster( try: values = file_study.tree.get(path.split("/"), depth=1) except KeyError: - raise ClusterNotFound(cluster_id) from None + raise RenewableClusterNotFound(path, cluster_id) from None else: old_config = create_renewable_config(study_version, **values) @@ -298,12 +298,12 @@ def duplicate_cluster( The duplicated cluster configuration. Raises: - ClusterAlreadyExists: If a cluster with the new name already exists in the area. + DuplicateRenewableCluster: If a cluster with the new name already exists in the area. """ new_id = transform_name_to_id(new_cluster_name, lower=False) lower_new_id = new_id.lower() if any(lower_new_id == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): - raise ClusterAlreadyExists("Renewable", new_id) + raise DuplicateRenewableCluster(area_id, new_id) # Cluster duplication current_cluster = self.get_cluster(study, area_id, source_id) diff --git a/antarest/study/business/areas/st_storage_management.py b/antarest/study/business/areas/st_storage_management.py index 7109d8c668..c6d4e9f868 100644 --- a/antarest/study/business/areas/st_storage_management.py +++ b/antarest/study/business/areas/st_storage_management.py @@ -5,16 +5,15 @@ import numpy as np from pydantic import BaseModel, Extra, root_validator, validator +from requests.structures import CaseInsensitiveDict from typing_extensions import Literal from antarest.core.exceptions import ( AreaNotFound, - ClusterAlreadyExists, - DuplicateSTStorageId, - STStorageConfigNotFoundError, - STStorageFieldsNotFoundError, - STStorageMatrixNotFoundError, - STStorageNotFoundError, + DuplicateSTStorage, + STStorageConfigNotFound, + STStorageMatrixNotFound, + STStorageNotFound, ) from antarest.study.business.utils import AllOptionalMetaclass, camel_case_model, execute_or_add_commands from antarest.study.model import Study @@ -26,6 +25,7 @@ create_st_storage_config, ) from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.rawstudy.model.filesystem.folder_node import ChildNotFoundError from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_st_storage import CreateSTStorage from antarest.study.storage.variantstudy.model.command.remove_st_storage import RemoveSTStorage @@ -227,8 +227,18 @@ def validate_rule_curve( # ============================ -STORAGE_LIST_PATH = "input/st-storage/clusters/{area_id}/list/{storage_id}" -STORAGE_SERIES_PATH = "input/st-storage/series/{area_id}/{storage_id}/{ts_name}" +_STORAGE_LIST_PATH = "input/st-storage/clusters/{area_id}/list/{storage_id}" +_STORAGE_SERIES_PATH = "input/st-storage/series/{area_id}/{storage_id}/{ts_name}" + + +def _get_values_by_ids(file_study: FileStudy, area_id: str) -> t.Mapping[str, t.Mapping[str, t.Any]]: + path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id="")[:-1] + try: + return CaseInsensitiveDict(file_study.tree.get(path.split("/"), depth=3)) + except ChildNotFoundError: + raise AreaNotFound(area_id) from None + except KeyError: + raise STStorageConfigNotFound(path, area_id) from None class STStorageManager: @@ -264,8 +274,13 @@ def create_storage( The ID of the newly created short-term storage. """ file_study = self._get_file_study(study) + values_by_ids = _get_values_by_ids(file_study, area_id) + storage = form.to_config(study.version) - _check_creation_feasibility(file_study, area_id, storage.id) + values = values_by_ids.get(storage.id) + if values is not None: + raise DuplicateSTStorage(area_id, storage.id) + command = self._make_create_cluster_cmd(area_id, storage) execute_or_add_commands( study, @@ -301,11 +316,13 @@ def get_storages( """ file_study = self._get_file_study(study) - path = STORAGE_LIST_PATH.format(area_id=area_id, storage_id="")[:-1] + path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id="")[:-1] try: config = file_study.tree.get(path.split("/"), depth=3) + except ChildNotFoundError: + raise AreaNotFound(area_id) from None except KeyError: - raise STStorageConfigNotFoundError(study.id, area_id) from None + raise STStorageConfigNotFound(path, area_id) from None # Sort STStorageConfig by groups and then by name order_by = operator.attrgetter("group", "name") @@ -334,11 +351,11 @@ def get_storage( """ file_study = self._get_file_study(study) - path = STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) + path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) try: config = file_study.tree.get(path.split("/"), depth=1) except KeyError: - raise STStorageFieldsNotFoundError(storage_id) from None + raise STStorageNotFound(path, storage_id) from None return STStorageOutput.from_config(storage_id, config) def update_storage( @@ -365,15 +382,13 @@ def update_storage( # But sadly, there's no other way to prevent creating wrong commands. file_study = self._get_file_study(study) - _check_update_feasibility(file_study, area_id, storage_id) + values_by_ids = _get_values_by_ids(file_study, area_id) - path = STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) - try: - values = file_study.tree.get(path.split("/"), depth=1) - except KeyError: - raise STStorageFieldsNotFoundError(storage_id) from None - else: - old_config = create_st_storage_config(study_version, **values) + values = values_by_ids.get(storage_id) + if values is None: + path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) + raise STStorageNotFound(path, storage_id) + old_config = create_st_storage_config(study_version, **values) # use Python values to synchronize Config and Form values new_values = form.dict(by_alias=False, exclude_none=True) @@ -389,6 +404,7 @@ def update_storage( # create the update config commands with the modified data command_context = self.storage_service.variant_study_service.command_factory.command_context + path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) commands = [ UpdateConfig(target=f"{path}/{key}", data=value, command_context=command_context) for key, value in data.items() @@ -413,7 +429,12 @@ def delete_storages( storage_ids: IDs list of short-term storages to remove. """ file_study = self._get_file_study(study) - _check_deletion_feasibility(file_study, area_id, storage_ids) + values_by_ids = _get_values_by_ids(file_study, area_id) + + for storage_id in storage_ids: + if storage_id not in values_by_ids: + path = _STORAGE_LIST_PATH.format(area_id=area_id, storage_id=storage_id) + raise STStorageNotFound(path, storage_id) command_context = self.storage_service.variant_study_service.command_factory.command_context for storage_id in storage_ids: @@ -443,7 +464,7 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus new_id = transform_name_to_id(new_cluster_name) lower_new_id = new_id.lower() if any(lower_new_id == storage.id.lower() for storage in self.get_storages(study, area_id)): - raise ClusterAlreadyExists("Short-term storage", new_id) + raise DuplicateSTStorage(area_id, new_id) # Cluster duplication current_cluster = self.get_storage(study, area_id, source_id) @@ -457,11 +478,11 @@ def duplicate_cluster(self, study: Study, area_id: str, source_id: str, new_clus # noinspection SpellCheckingInspection ts_names = ["pmax_injection", "pmax_withdrawal", "lower_rule_curve", "upper_rule_curve", "inflows"] source_paths = [ - STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=lower_source_id, ts_name=ts_name) + _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=lower_source_id, ts_name=ts_name) for ts_name in ts_names ] new_paths = [ - STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=lower_new_id, ts_name=ts_name) + _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=lower_new_id, ts_name=ts_name) for ts_name in ts_names ] @@ -508,11 +529,11 @@ def _get_matrix_obj( ts_name: STStorageTimeSeries, ) -> t.MutableMapping[str, t.Any]: file_study = self._get_file_study(study) - path = STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) + path = _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) try: matrix = file_study.tree.get(path.split("/"), depth=1) except KeyError: - raise STStorageMatrixNotFoundError(study.id, area_id, storage_id, ts_name) from None + raise STStorageMatrixNotFound(path) from None return matrix def update_matrix( @@ -545,7 +566,7 @@ def _save_matrix_obj( ) -> None: file_study = self._get_file_study(study) command_context = self.storage_service.variant_study_service.command_factory.command_context - path = STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) + path = _STORAGE_SERIES_PATH.format(area_id=area_id, storage_id=storage_id, ts_name=ts_name) command = ReplaceMatrix(target=path, matrix=matrix_data, command_context=command_context) execute_or_add_commands(study, file_study, [command], self.storage_service) @@ -592,31 +613,3 @@ def validate_matrices( # Validation successful return True - - -def _get_existing_storage_ids(file_study: FileStudy, area_id: str) -> t.Set[str]: - try: - area = file_study.config.areas[area_id] - except KeyError: - raise AreaNotFound(area_id) from None - else: - return {s.id for s in area.st_storages} - - -def _check_deletion_feasibility(file_study: FileStudy, area_id: str, storage_ids: t.Sequence[str]) -> None: - existing_ids = _get_existing_storage_ids(file_study, area_id) - for storage_id in storage_ids: - if storage_id not in existing_ids: - raise STStorageNotFoundError(file_study.config.study_id, area_id, storage_id) - - -def _check_update_feasibility(file_study: FileStudy, area_id: str, storage_id: str) -> None: - existing_ids = _get_existing_storage_ids(file_study, area_id) - if storage_id not in existing_ids: - raise STStorageNotFoundError(file_study.config.study_id, area_id, storage_id) - - -def _check_creation_feasibility(file_study: FileStudy, area_id: str, storage_id: str) -> None: - existing_ids = _get_existing_storage_ids(file_study, area_id) - if storage_id in existing_ids: - raise DuplicateSTStorageId(file_study.config.study_id, area_id, storage_id) diff --git a/antarest/study/business/areas/thermal_management.py b/antarest/study/business/areas/thermal_management.py index 5a106e7fa7..9fea2c568d 100644 --- a/antarest/study/business/areas/thermal_management.py +++ b/antarest/study/business/areas/thermal_management.py @@ -3,7 +3,7 @@ from pydantic import validator -from antarest.core.exceptions import ClusterAlreadyExists, ClusterConfigNotFound, ClusterNotFound +from antarest.core.exceptions import DuplicateThermalCluster, ThermalClusterConfigNotFound, ThermalClusterNotFound 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.model import transform_name_to_id @@ -138,7 +138,7 @@ def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> ThermalClu The cluster with the specified ID. Raises: - ClusterNotFound: If the specified cluster does not exist. + ThermalClusterNotFound: If the specified cluster does not exist. """ file_study = self._get_file_study(study) @@ -146,7 +146,7 @@ def get_cluster(self, study: Study, area_id: str, cluster_id: str) -> ThermalClu try: cluster = file_study.tree.get(path.split("/"), depth=1) except KeyError: - raise ClusterNotFound(cluster_id) + raise ThermalClusterNotFound(path, cluster_id) from None study_version = study.version return create_thermal_output(study_version, cluster_id, cluster) @@ -166,7 +166,7 @@ def get_clusters( A list of thermal clusters within the specified area. Raises: - ClusterConfigNotFound: If no clusters are found in the specified area. + ThermalClusterConfigNotFound: If no clusters are found in the specified area. """ file_study = self._get_file_study(study) @@ -174,7 +174,7 @@ def get_clusters( try: clusters = file_study.tree.get(path.split("/"), depth=3) except KeyError: - raise ClusterConfigNotFound(area_id) + raise ThermalClusterConfigNotFound(path, area_id) from None study_version = study.version return [create_thermal_output(study_version, cluster_id, cluster) for cluster_id, cluster in clusters.items()] @@ -235,7 +235,7 @@ def update_cluster( The updated cluster. Raises: - ClusterNotFound: If the provided `cluster_id` does not match the ID of the cluster + ThermalClusterNotFound: If the provided `cluster_id` does not match the ID of the cluster in the provided cluster_data. """ @@ -245,7 +245,7 @@ def update_cluster( try: values = file_study.tree.get(path.split("/"), depth=1) except KeyError: - raise ClusterNotFound(cluster_id) from None + raise ThermalClusterNotFound(path, cluster_id) from None else: old_config = create_thermal_config(study_version, **values) @@ -317,7 +317,7 @@ def duplicate_cluster( new_id = transform_name_to_id(new_cluster_name, lower=False) lower_new_id = new_id.lower() if any(lower_new_id == cluster.id.lower() for cluster in self.get_clusters(study, area_id)): - raise ClusterAlreadyExists("Thermal", new_id) + raise DuplicateThermalCluster(area_id, new_id) # Cluster duplication source_cluster = self.get_cluster(study, area_id, source_id) diff --git a/tests/integration/study_data_blueprint/test_renewable.py b/tests/integration/study_data_blueprint/test_renewable.py index 8447c0430f..0e57e1464b 100644 --- a/tests/integration/study_data_blueprint/test_renewable.py +++ b/tests/integration/study_data_blueprint/test_renewable.py @@ -475,8 +475,8 @@ def test_lifecycle( ) assert res.status_code == 404 obj = res.json() - assert obj["description"] == f"Cluster: '{unknown_id}' not found" - assert obj["exception"] == "ClusterNotFound" + assert f"'{unknown_id}' not found" in obj["description"] + assert obj["exception"] == "RenewableClusterNotFound" # Cannot duplicate with an existing id res = client.post( @@ -488,7 +488,7 @@ def test_lifecycle( obj = res.json() description = obj["description"] assert other_cluster_name.upper() in description - assert obj["exception"] == "ClusterAlreadyExists" + assert obj["exception"] == "DuplicateRenewableCluster" @pytest.fixture(name="base_study_id") def base_study_id_fixture(self, request: t.Any, client: TestClient, user_access_token: str) -> str: diff --git a/tests/integration/study_data_blueprint/test_st_storage.py b/tests/integration/study_data_blueprint/test_st_storage.py index 161e6417b8..9566c4ba71 100644 --- a/tests/integration/study_data_blueprint/test_st_storage.py +++ b/tests/integration/study_data_blueprint/test_st_storage.py @@ -477,8 +477,10 @@ def test_lifecycle__nominal( ) assert res.status_code == 404 obj = res.json() - assert obj["description"] == f"Short-term storage '{bad_storage_id}' not found in area '{area_id}'" - assert obj["exception"] == "STStorageNotFoundError" + description = obj["description"] + assert bad_storage_id in description + assert re.search(r"'bad_storage'", description, flags=re.IGNORECASE) + assert re.search(r"not found", description, flags=re.IGNORECASE) # Check PATCH with the wrong `study_id` res = client.patch( @@ -500,8 +502,8 @@ def test_lifecycle__nominal( ) assert res.status_code == 404, res.json() obj = res.json() - assert obj["description"] == f"Fields of storage '{unknown_id}' not found" - assert obj["exception"] == "STStorageFieldsNotFoundError" + assert f"'{unknown_id}' not found" in obj["description"] + assert obj["exception"] == "STStorageNotFound" # Cannot duplicate with an existing id res = client.post( @@ -513,7 +515,7 @@ def test_lifecycle__nominal( obj = res.json() description = obj["description"] assert siemens_battery.lower() in description - assert obj["exception"] == "ClusterAlreadyExists" + assert obj["exception"] == "DuplicateSTStorage" @pytest.mark.parametrize("study_type", ["raw", "variant"]) def test__default_values(self, client: TestClient, user_access_token: str, study_type: str) -> None: diff --git a/tests/integration/study_data_blueprint/test_thermal.py b/tests/integration/study_data_blueprint/test_thermal.py index a44d7058ac..46b7a3956b 100644 --- a/tests/integration/study_data_blueprint/test_thermal.py +++ b/tests/integration/study_data_blueprint/test_thermal.py @@ -745,8 +745,8 @@ def test_lifecycle( ) assert res.status_code == 404, res.json() obj = res.json() - assert obj["description"] == f"Cluster: '{unknown_id}' not found" - assert obj["exception"] == "ClusterNotFound" + assert f"'{unknown_id}' not found" in obj["description"] + assert obj["exception"] == "ThermalClusterNotFound" # Cannot duplicate with an existing id res = client.post( @@ -758,7 +758,7 @@ def test_lifecycle( obj = res.json() description = obj["description"] assert new_name.upper() in description - assert obj["exception"] == "ClusterAlreadyExists" + assert obj["exception"] == "DuplicateThermalCluster" @pytest.fixture(name="base_study_id") def base_study_id_fixture(self, request: t.Any, client: TestClient, user_access_token: str) -> str: diff --git a/tests/study/business/areas/test_st_storage_management.py b/tests/study/business/areas/test_st_storage_management.py index 5c3e7e660c..ee9a79287e 100644 --- a/tests/study/business/areas/test_st_storage_management.py +++ b/tests/study/business/areas/test_st_storage_management.py @@ -10,20 +10,14 @@ from pydantic import ValidationError from sqlalchemy.orm.session import Session # type: ignore -from antarest.core.exceptions import ( - AreaNotFound, - STStorageConfigNotFoundError, - STStorageFieldsNotFoundError, - STStorageMatrixNotFoundError, - STStorageNotFoundError, -) +from antarest.core.exceptions import AreaNotFound, STStorageConfigNotFound, STStorageMatrixNotFound, STStorageNotFound from antarest.core.model import PublicMode from antarest.login.model import Group, User from antarest.study.business.areas.st_storage_management import STStorageInput, STStorageManager from antarest.study.model import RawStudy, Study, StudyContentStatus from antarest.study.storage.rawstudy.ini_reader import IniReader -from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, FileStudyTreeConfig -from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageConfig, STStorageGroup +from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.config.st_storage import STStorageGroup from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode from antarest.study.storage.rawstudy.model.filesystem.root.filestudytree import FileStudyTree @@ -183,7 +177,7 @@ def test_get_st_storages__config_not_found( This test verifies that when the `get_storages` method is called with a study and area ID, and the corresponding configuration is not found (indicated by the `KeyError` raised by the mock), it correctly - raises the `STStorageConfigNotFoundError` exception with the expected error + raises the `STStorageConfigNotFound` exception with the expected error message containing the study ID and area ID. """ # The study must be fetched from the database @@ -201,7 +195,7 @@ def test_get_st_storages__config_not_found( manager = STStorageManager(study_storage_service) # run - with pytest.raises(STStorageConfigNotFoundError, match="not found") as ctx: + with pytest.raises(STStorageConfigNotFound, match="not found") as ctx: manager.get_storages(study, area_id="West") # ensure the error message contains at least the study ID and area ID @@ -286,11 +280,10 @@ def test_update_storage__nominal_case( ini_file_node = IniFileNode(context=Mock(), config=Mock()) file_study.tree = Mock( spec=FileStudyTree, - get=Mock(return_value=LIST_CFG["storage1"]), + get=Mock(return_value=LIST_CFG), get_node=Mock(return_value=ini_file_node), ) - area = Mock(spec=Area) mock_config = Mock(spec=FileStudyTreeConfig, study_id=study.id) file_study.config = mock_config @@ -299,20 +292,22 @@ def test_update_storage__nominal_case( edit_form = STStorageInput(initial_level=0, initial_level_optim=False) # Test behavior for area not in study - mock_config.areas = {"fake_area": area} - with pytest.raises(AreaNotFound) as ctx: - manager.update_storage(study, area_id="West", storage_id="storage1", form=edit_form) - assert ctx.value.detail == "Area is not found: 'West'" + # noinspection PyTypeChecker + file_study.tree.get.return_value = {} + with pytest.raises((AreaNotFound, STStorageNotFound)) as ctx: + manager.update_storage(study, area_id="unknown_area", storage_id="storage1", form=edit_form) + assert "unknown_area" in ctx.value.detail + assert "storage1" in ctx.value.detail # Test behavior for st_storage not in study - mock_config.areas = {"West": area} - area.st_storages = [STStorageConfig(name="fake_name", group="battery")] - with pytest.raises(STStorageNotFoundError) as ctx: - manager.update_storage(study, area_id="West", storage_id="storage1", form=edit_form) - assert ctx.value.detail == "Short-term storage 'storage1' not found in area 'West'" + file_study.tree.get.return_value = {"storage1": LIST_CFG["storage1"]} + with pytest.raises(STStorageNotFound) as ctx: + manager.update_storage(study, area_id="West", storage_id="unknown_storage", form=edit_form) + assert "West" in ctx.value.detail + assert "unknown_storage" in ctx.value.detail # Test behavior for nominal case - area.st_storages = [STStorageConfig(name="storage1", group="battery")] + file_study.tree.get.return_value = LIST_CFG manager.update_storage(study, area_id="West", storage_id="storage1", form=edit_form) # Assert that the storage fields have been updated @@ -351,7 +346,7 @@ def test_get_st_storage__config_not_found( """ Test the `get_st_storage` method of the `STStorageManager` class when the configuration is not found. - This test verifies that the `get_st_storage` method raises an `STStorageFieldsNotFoundError` + This test verifies that the `get_st_storage` method raises an `STStorageNotFound` exception when the configuration for the provided study, area, and storage ID combination is not found. Args: @@ -375,7 +370,7 @@ def test_get_st_storage__config_not_found( manager = STStorageManager(study_storage_service) # Run the method being tested and expect an exception - with pytest.raises(STStorageFieldsNotFoundError, match="not found") as ctx: + with pytest.raises(STStorageNotFound, match="not found") as ctx: manager.get_storage(study, area_id="West", storage_id="storage1") # ensure the error message contains at least the study ID, area ID and storage ID err_msg = str(ctx.value) @@ -436,7 +431,7 @@ def test_get_matrix__config_not_found( """ Test the `get_matrix` method of the `STStorageManager` class when the time series is not found. - This test verifies that the `get_matrix` method raises an `STStorageFieldsNotFoundError` + This test verifies that the `get_matrix` method raises an `STStorageNotFound` exception when the configuration for the provided study, area, time series, and storage ID combination is not found. @@ -461,7 +456,7 @@ def test_get_matrix__config_not_found( manager = STStorageManager(study_storage_service) # Run the method being tested and expect an exception - with pytest.raises(STStorageMatrixNotFoundError, match="not found") as ctx: + with pytest.raises(STStorageMatrixNotFound, match="not found") as ctx: manager.get_matrix(study, area_id="West", storage_id="storage1", ts_name="inflows") # ensure the error message contains at least the study ID, area ID and storage ID err_msg = str(ctx.value) @@ -477,7 +472,7 @@ def test_get_matrix__invalid_matrix( """ Test the `get_matrix` method of the `STStorageManager` class when the time series is not found. - This test verifies that the `get_matrix` method raises an `STStorageFieldsNotFoundError` + This test verifies that the `get_matrix` method raises an `STStorageNotFound` exception when the configuration for the provided study, area, time series, and storage ID combination is not found.