From 8d4fb3cb8c2a20fd1bed65cf22becff578a5e25e Mon Sep 17 00:00:00 2001 From: Salem <97806715+salemsd@users.noreply.github.com> Date: Thu, 9 Jan 2025 11:14:45 +0100 Subject: [PATCH] feat(api): add delete_output and delete_outputs methods (#49) --- src/antares/craft/exceptions/exceptions.py | 6 +++ src/antares/craft/model/study.py | 8 +++ .../craft/service/api_services/study_api.py | 21 ++++++++ src/antares/craft/service/base_services.py | 17 +++++++ .../service/local_services/study_local.py | 6 +++ .../services/api_services/test_study_api.py | 51 +++++++++++++++++++ tests/integration/test_web_client.py | 21 ++++++++ 7 files changed, 130 insertions(+) diff --git a/src/antares/craft/exceptions/exceptions.py b/src/antares/craft/exceptions/exceptions.py index 18032f38..18b0a80b 100644 --- a/src/antares/craft/exceptions/exceptions.py +++ b/src/antares/craft/exceptions/exceptions.py @@ -330,6 +330,12 @@ def __init__(self, study_id: str, message: str) -> None: super().__init__(self.message) +class OutputDeletionError(Exception): + def __init__(self, study_id: str, output_name: str, message: str) -> None: + self.message = f"Could not delete the output {output_name} from study {study_id}: " + message + super().__init__(self.message) + + class ConstraintRetrievalError(Exception): def __init__(self, study_id: str, message: str) -> None: self.message = f"Could not get binding constraints for {study_id}: " + message diff --git a/src/antares/craft/model/study.py b/src/antares/craft/model/study.py index 7913cbbb..7af891ef 100644 --- a/src/antares/craft/model/study.py +++ b/src/antares/craft/model/study.py @@ -411,6 +411,14 @@ def get_output(self, output_id: str) -> Output: """ return self._outputs[output_id] + def delete_outputs(self) -> None: + self._study_service.delete_outputs() + self._outputs.clear() + + def delete_output(self, output_name: str) -> None: + self._study_service.delete_output(output_name) + self._outputs.pop(output_name) + def _verify_study_already_exists(study_directory: Path) -> None: if study_directory.exists(): diff --git a/src/antares/craft/service/api_services/study_api.py b/src/antares/craft/service/api_services/study_api.py index 033524ba..4e723f1e 100644 --- a/src/antares/craft/service/api_services/study_api.py +++ b/src/antares/craft/service/api_services/study_api.py @@ -18,6 +18,7 @@ from antares.craft.exceptions.exceptions import ( APIError, BindingConstraintDeletionError, + OutputDeletionError, OutputsRetrievalError, StudyDeletionError, StudySettingsUpdateError, @@ -141,3 +142,23 @@ def read_outputs(self) -> list[Output]: ] except APIError as e: raise OutputsRetrievalError(self.study_id, e.message) + + def delete_outputs(self) -> None: + outputs_url = f"{self._base_url}/studies/{self.study_id}/outputs" + try: + response = self._wrapper.get(outputs_url) + outputs_json_list = response.json() + if not outputs_json_list: + raise OutputsRetrievalError(self.study_id, "No outputs to delete.") + for output in outputs_json_list: + output_name = output["name"] + self.delete_output(output_name) + except APIError as e: + raise OutputsRetrievalError(self.study_id, e.message) + + def delete_output(self, output_name: str) -> None: + url = f"{self._base_url}/studies/{self.study_id}/outputs/{output_name}" + try: + self._wrapper.delete(url) + except APIError as e: + raise OutputDeletionError(self.study_id, output_name, e.message) from e diff --git a/src/antares/craft/service/base_services.py b/src/antares/craft/service/base_services.py index 261bc47b..722df7a3 100644 --- a/src/antares/craft/service/base_services.py +++ b/src/antares/craft/service/base_services.py @@ -599,6 +599,23 @@ def read_outputs(self) -> list[Output]: def set_output_service(self, output_service: "BaseOutputService") -> None: pass + @abstractmethod + def delete_outputs(self) -> None: + """ + Deletes all the outputs of the study + """ + pass + + @abstractmethod + def delete_output(self, output_name: str) -> None: + """ + Deletes given output from the study + + Args: + output_name: To be deleted output + """ + pass + class BaseRenewableService(ABC): @abstractmethod diff --git a/src/antares/craft/service/local_services/study_local.py b/src/antares/craft/service/local_services/study_local.py index fd0266d8..d409f496 100644 --- a/src/antares/craft/service/local_services/study_local.py +++ b/src/antares/craft/service/local_services/study_local.py @@ -58,3 +58,9 @@ def create_variant(self, variant_name: str) -> "Study": def read_outputs(self) -> list[Output]: raise NotImplementedError + + def delete_outputs(self) -> None: + raise NotImplementedError + + def delete_output(self, output_name: str) -> None: + raise NotImplementedError diff --git a/tests/antares/services/api_services/test_study_api.py b/tests/antares/services/api_services/test_study_api.py index ec23c455..5addc0d4 100644 --- a/tests/antares/services/api_services/test_study_api.py +++ b/tests/antares/services/api_services/test_study_api.py @@ -26,6 +26,7 @@ BindingConstraintCreationError, ConstraintRetrievalError, LinkCreationError, + OutputDeletionError, OutputsRetrievalError, SimulationFailedError, SimulationTimeOutError, @@ -597,3 +598,53 @@ def test_output_aggregate_values(self): expected_matrix = pd.read_csv(StringIO(aggregate_output)) assert isinstance(aggregated_matrix, pd.DataFrame) assert aggregated_matrix.equals(expected_matrix) + + def test_delete_output(self): + output_name = "test_output" + with requests_mock.Mocker() as mocker: + outputs_url = f"https://antares.com/api/v1/studies/{self.study_id}/outputs" + delete_url = f"{outputs_url}/{output_name}" + mocker.get(outputs_url, json=[{"name": output_name, "archived": False}], status_code=200) + mocker.delete(delete_url, status_code=200) + + self.study.read_outputs() + assert output_name in self.study.get_outputs() + self.study.delete_output(output_name) + assert output_name not in self.study.get_outputs() + + # failing + error_message = f"Output {output_name} deletion failed" + mocker.delete(delete_url, json={"description": error_message}, status_code=404) + + with pytest.raises(OutputDeletionError, match=error_message): + self.study.delete_output(output_name) + + def test_delete_outputs(self): + with requests_mock.Mocker() as mocker: + outputs_url = f"https://antares.com/api/v1/studies/{self.study_id}/outputs" + outputs_json = [ + {"name": "output1", "archived": False}, + {"name": "output2", "archived": True}, + ] + mocker.get(outputs_url, json=outputs_json, status_code=200) + + delete_url1 = f"https://antares.com/api/v1/studies/{self.study_id}/outputs/output1" + delete_url2 = f"https://antares.com/api/v1/studies/{self.study_id}/outputs/output2" + mocker.delete(delete_url1, status_code=200) + mocker.delete(delete_url2, status_code=200) + + mocker.get( + outputs_url, + json=[{"name": "output1", "archived": False}, {"name": "output2", "archived": True}], + status_code=200, + ) + assert len(self.study.read_outputs()) == 2 + + self.study.delete_outputs() + assert len(self.study.get_outputs()) == 0 + + # failing (nothing to delete) + error_message = "Outputs deletion failed" + mocker.get(outputs_url, json={"description": error_message}, status_code=404) + with pytest.raises(OutputsRetrievalError, match=error_message): + self.study.delete_outputs() diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index 299ad1c4..a707918f 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -578,3 +578,24 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop): assert cluster_term.data.cluster == "cluster_test" assert cluster_term.weight == 4.5 assert cluster_term.offset == 3 + + # ===== Output deletion ===== + + # run two new simulations for creating more outputs + study.wait_job_completion( + study.run_antares_simulation(AntaresSimulationParameters(output_suffix="2")), time_out=60 + ) + study.wait_job_completion( + study.run_antares_simulation(AntaresSimulationParameters(output_suffix="3")), time_out=60 + ) + assert len(study.read_outputs()) == 3 + + # delete_output + study.delete_output(output.name) + assert output.name not in study.get_outputs() + assert len(study.read_outputs()) == 2 + + # delete_outputs + study.delete_outputs() + assert len(study.get_outputs()) == 0 + assert len(study.read_outputs()) == 0