Skip to content

Commit

Permalink
feat(api): add possibility to move a study (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
salemsd authored Jan 14, 2025
1 parent 8208053 commit b5e69d1
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 9 deletions.
6 changes: 6 additions & 0 deletions src/antares/craft/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,12 @@ def __init__(self, study_name: str, message: str) -> None:
super().__init__(self.message)


class StudyMoveError(Exception):
def __init__(self, study_id: str, new_folder_name: str, message: str) -> None:
self.message = f"Could not move the study {study_id} to folder {new_folder_name}: " + message
super().__init__(self.message)


class ThermalMatrixDownloadError(Exception):
def __init__(self, area_name: str, cluster_name: str, matrix_name: str, message: str) -> None:
self.message = (
Expand Down
35 changes: 28 additions & 7 deletions src/antares/craft/model/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import os
import time

from pathlib import Path
from pathlib import Path, PurePath
from types import MappingProxyType
from typing import List, Optional, Union

Expand All @@ -23,7 +23,7 @@
from antares.craft.api_conf.api_conf import APIconf
from antares.craft.api_conf.request_wrapper import RequestWrapper
from antares.craft.config.local_configuration import LocalConfiguration
from antares.craft.exceptions.exceptions import APIError, LinkCreationError, StudyCreationError
from antares.craft.exceptions.exceptions import APIError, LinkCreationError, StudyCreationError, StudyMoveError
from antares.craft.model.area import Area, AreaProperties, AreaUi
from antares.craft.model.binding_constraint import (
BindingConstraint,
Expand All @@ -50,7 +50,11 @@


def create_study_api(
study_name: str, version: str, api_config: APIconf, settings: Optional[StudySettings] = None
study_name: str,
version: str,
api_config: APIconf,
settings: Optional[StudySettings] = None,
parent_path: Optional[Path] = None,
) -> "Study":
"""
Args:
Expand All @@ -73,10 +77,16 @@ def create_study_api(
response = wrapper.post(url)
study_id = response.json()
study_settings = _returns_study_settings(base_url, study_id, wrapper, False, settings)

except APIError as e:
study = Study(study_name, version, ServiceFactory(api_config, study_id), study_settings)
if parent_path:
study.move(parent_path)
url = f"{base_url}/studies/{study_id}"
json_study = wrapper.get(url).json()
folder = json_study.pop("folder")
study.path = PurePath(folder) if folder else PurePath(".")
return study
except (APIError, StudyMoveError) as e:
raise StudyCreationError(study_name, e.message) from e
return Study(study_name, version, ServiceFactory(api_config, study_id), study_settings)


def create_study_local(
Expand Down Expand Up @@ -143,6 +153,7 @@ def create_study_local(
version=version,
service_factory=ServiceFactory(config=local_config, study_name=study_name),
settings=local_settings,
path=study_directory,
)


Expand Down Expand Up @@ -173,6 +184,7 @@ def _directory_not_exists(local_path: Path) -> None:
name=study_params["caption"],
version=study_params["version"],
service_factory=ServiceFactory(config=local_config, study_name=study_params["caption"]),
path=study_directory,
)


Expand All @@ -184,9 +196,13 @@ def read_study_api(api_config: APIconf, study_id: str) -> "Study":

study_name = json_study.pop("name")
study_version = str(json_study.pop("version"))
path = json_study.pop("folder")
pure_path = PurePath(path) if path else PurePath(".")

study_settings = _returns_study_settings(base_url, study_id, wrapper, False, None)
study = Study(study_name, study_version, ServiceFactory(api_config, study_id, study_name), study_settings)
study = Study(
study_name, study_version, ServiceFactory(api_config, study_id, study_name), study_settings, pure_path
)

study.read_areas()
study.read_outputs()
Expand Down Expand Up @@ -217,9 +233,11 @@ def __init__(
version: str,
service_factory: ServiceFactory,
settings: Union[StudySettings, StudySettingsLocal, None] = None,
path: PurePath = PurePath("."),
):
self.name = name
self.version = version
self.path = path
self._study_service = service_factory.create_study_service()
self._area_service = service_factory.create_area_service()
self._link_service = service_factory.create_link_service()
Expand Down Expand Up @@ -421,6 +439,9 @@ def delete_output(self, output_name: str) -> None:
self._study_service.delete_output(output_name)
self._outputs.pop(output_name)

def move(self, parent_path: Path) -> None:
self.path = self._study_service.move_study(parent_path)

def generate_thermal_timeseries(self) -> None:
self._study_service.generate_thermal_timeseries()

Expand Down
12 changes: 12 additions & 0 deletions src/antares/craft/service/api_services/study_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.
from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Optional

import antares.craft.model.study as study
Expand All @@ -21,6 +22,7 @@
OutputDeletionError,
OutputsRetrievalError,
StudyDeletionError,
StudyMoveError,
StudySettingsUpdateError,
StudyVariantCreationError,
TaskFailedError,
Expand Down Expand Up @@ -167,6 +169,16 @@ def delete_output(self, output_name: str) -> None:
except APIError as e:
raise OutputDeletionError(self.study_id, output_name, e.message) from e

def move_study(self, new_parent_path: Path) -> PurePath:
url = f"{self._base_url}/studies/{self.study_id}/move?folder_dest={new_parent_path}"
try:
self._wrapper.put(url)
json_study = self._wrapper.get(f"{self._base_url}/studies/{self.study_id}").json()
folder = json_study.pop("folder")
return PurePath(folder) if folder else PurePath(".")
except APIError as e:
raise StudyMoveError(self.study_id, new_parent_path.as_posix(), e.message) from e

def generate_thermal_timeseries(self) -> None:
url = f"{self._base_url}/studies/{self.study_id}/timeseries/generate"
try:
Expand Down
10 changes: 10 additions & 0 deletions src/antares/craft/service/base_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# This file is part of the Antares project.

from abc import ABC, abstractmethod
from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Dict, List, Optional

import pandas as pd
Expand Down Expand Up @@ -566,6 +567,15 @@ def create_variant(self, variant_name: str) -> "Study":
"""
pass

@abstractmethod
def move_study(self, new_parent_path: Path) -> PurePath:
"""
Moves the study to the new parent path
Returns: the new path
"""
pass

@abstractmethod
def read_outputs(self) -> list[Output]:
"""
Expand Down
5 changes: 4 additions & 1 deletion src/antares/craft/service/local_services/study_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.

from pathlib import Path, PurePath
from typing import TYPE_CHECKING, Any, Optional

from antares.craft.config.local_configuration import LocalConfiguration
Expand Down Expand Up @@ -65,5 +65,8 @@ def delete_outputs(self) -> None:
def delete_output(self, output_name: str) -> None:
raise NotImplementedError

def move_study(self, new_parent_path: Path) -> PurePath:
raise NotImplementedError

def generate_thermal_timeseries(self) -> None:
raise NotImplementedError
38 changes: 37 additions & 1 deletion tests/antares/services/api_services/test_study_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from io import StringIO
from json import dumps
from pathlib import Path, PurePath
from unittest.mock import Mock, patch

import pandas as pd
Expand All @@ -31,6 +32,7 @@
SimulationFailedError,
SimulationTimeOutError,
StudyCreationError,
StudyMoveError,
StudySettingsUpdateError,
StudyVariantCreationError,
ThermalTimeseriesGenerationError,
Expand Down Expand Up @@ -74,6 +76,17 @@ def test_create_study_test_ok(self) -> None:
mocker.post(expected_url, json=self.study_id, status_code=200)
config_urls = re.compile(f"https://antares.com/api/v1/studies/{self.study_id}/config/.*")
mocker.get(config_urls, json={}, status_code=200)
expected_url_path = f"https://antares.com/api/v1/studies/{self.study_id}"
mocker.get(
expected_url_path,
json={
"id": f"{self.study_id}",
"name": f"{self.study.name}",
"version": f"{self.study.version}",
"folder": None,
},
status_code=200,
)
# When
study = create_study_api("TestStudy", "880", self.api)

Expand Down Expand Up @@ -201,6 +214,7 @@ def test_read_study_api(self):
"id": "22c52f44-4c2a-407b-862b-490887f93dd8",
"name": "test_read_areas",
"version": "880",
"folder": None,
}

json_ui = {
Expand Down Expand Up @@ -263,7 +277,11 @@ def test_create_variant_success(self):
mocker.post(url, json=variant_id, status_code=201)

variant_url = f"{base_url}/studies/{variant_id}"
mocker.get(variant_url, json={"id": variant_id, "name": variant_name, "version": "880"}, status_code=200)
mocker.get(
variant_url,
json={"id": variant_id, "name": variant_name, "version": "880", "folder": None},
status_code=200,
)

config_urls = re.compile(f"{base_url}/studies/{variant_id}/config/.*")
mocker.get(config_urls, json={}, status_code=200)
Expand Down Expand Up @@ -650,6 +668,24 @@ def test_delete_outputs(self):
with pytest.raises(OutputsRetrievalError, match=error_message):
self.study.delete_outputs()

def test_move_study(self):
new_path = Path("/new/path/test")
with requests_mock.Mocker() as mocker:
move_url = f"https://antares.com/api/v1/studies/{self.study_id}/move?folder_dest={new_path}"
study_url = f"https://antares.com/api/v1/studies/{self.study_id}"
mocker.put(move_url, status_code=200)
mocker.get(study_url, json={"folder": f"/new/path/test/{self.study_id}"}, status_code=200)

assert self.study.path == PurePath(".")
self.study.move(new_path)
assert self.study.path == PurePath(new_path) / f"{self.study_id}"

# Failing
error_message = "Study move failed"
mocker.put(move_url, json={"description": error_message}, status_code=404)
with pytest.raises(StudyMoveError, match=error_message):
self.study.move(new_path)

def test_generate_thermal_timeseries_success(self):
with requests_mock.Mocker() as mocker:
url = f"https://antares.com/api/v1/studies/{self.study_id}/timeseries/generate"
Expand Down
13 changes: 13 additions & 0 deletions tests/integration/test_web_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# This file is part of the Antares project.
import pytest

from pathlib import Path, PurePath

import numpy as np
import pandas as pd

Expand Down Expand Up @@ -620,3 +622,14 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop):
study.delete_outputs()
assert len(study.get_outputs()) == 0
assert len(study.read_outputs()) == 0

# ===== Test study moving =====

new_path = Path("/new/path/test")
assert study.path == PurePath(".")
study.move(new_path)
assert study.path == PurePath(new_path) / f"{study.service.study_id}"

moved_study = read_study_api(api_config, study.service.study_id)
assert moved_study.path == study.path
assert moved_study.name == study.name

0 comments on commit b5e69d1

Please sign in to comment.