diff --git a/src/antares/craft/exceptions/exceptions.py b/src/antares/craft/exceptions/exceptions.py index 18b0a80b..ef8e5a72 100644 --- a/src/antares/craft/exceptions/exceptions.py +++ b/src/antares/craft/exceptions/exceptions.py @@ -78,6 +78,12 @@ def __init__(self, link_name: str, message: str) -> None: super().__init__(self.message) +class LinksRetrievalError(Exception): + def __init__(self, study_id: str, message: str) -> None: + self.message = f"Could not retrieve links from study {study_id} : {message}" + super().__init__(self.message) + + class ThermalCreationError(Exception): def __init__(self, thermal_name: str, area_id: str, message: str) -> None: self.message = f"Could not create the thermal cluster {thermal_name} inside area {area_id}: " + message diff --git a/src/antares/craft/model/link.py b/src/antares/craft/model/link.py index 73a47359..b7f00141 100644 --- a/src/antares/craft/model/link.py +++ b/src/antares/craft/model/link.py @@ -68,6 +68,7 @@ class DefaultLinkProperties(BaseModel, extra="forbid", populate_by_name=True, al FilterOption.MONTHLY, FilterOption.ANNUAL, } + comments: str = "" @all_optional_model @@ -89,6 +90,7 @@ def ini_fields(self) -> Mapping[str, str]: "filter-year-by-year": ", ".join( filter_value for filter_value in sort_filter_values(self.filter_year_by_year) ), + "comments": f"{self.comments}".lower(), } def yield_link_properties(self) -> LinkProperties: diff --git a/src/antares/craft/model/study.py b/src/antares/craft/model/study.py index 7af891ef..16fc83fe 100644 --- a/src/antares/craft/model/study.py +++ b/src/antares/craft/model/study.py @@ -245,6 +245,8 @@ def read_areas(self) -> list[Area]: return area_list def read_links(self) -> list[Link]: + link_list = self._link_service.read_links() + self._links = {link.id: link for link in link_list} return self._link_service.read_links() def get_areas(self) -> MappingProxyType[str, Area]: diff --git a/src/antares/craft/service/api_services/link_api.py b/src/antares/craft/service/api_services/link_api.py index 30627d0f..e91aaed9 100644 --- a/src/antares/craft/service/api_services/link_api.py +++ b/src/antares/craft/service/api_services/link_api.py @@ -10,7 +10,7 @@ # # This file is part of the Antares project. -from typing import Optional +from typing import Any, Optional import pandas as pd @@ -22,6 +22,7 @@ LinkDeletionError, LinkDownloadError, LinkPropertiesUpdateError, + LinksRetrievalError, LinkUiUpdateError, LinkUploadError, ) @@ -214,7 +215,47 @@ def create_capacity_indirect(self, series: pd.DataFrame, area_from: str, area_to raise LinkUploadError(area_from, area_to, "indirectcapacity", e.message) from e def read_links(self) -> list[Link]: - raise NotImplementedError + try: + url = f"{self._base_url}/studies/{self.study_id}/links" + json_links = self._wrapper.get(url).json() + links = [] + for link in json_links: + link_object = self.convert_api_link_to_internal_link(link) + links.append(link_object) + + links.sort(key=lambda link_obj: link_obj.area_from_id) + except APIError as e: + raise LinksRetrievalError(self.study_id, e.message) from e + return links + + def convert_api_link_to_internal_link(self, api_link: dict[str, Any]) -> Link: + link_area_from_id = api_link.pop("area1") + link_area_to_id = api_link.pop("area2") + + link_style = api_link.pop("linkStyle") + link_width = api_link.pop("linkWidth") + color_r = api_link.pop("colorr") + color_g = api_link.pop("colorg") + color_b = api_link.pop("colorb") + + link_ui = LinkUi(link_style=link_style, link_width=link_width, colorr=color_r, colorg=color_g, colorb=color_b) + + mapping = { + "hurdlesCost": "hurdles-cost", + "loopFlow": "loop-flow", + "usePhaseShifter": "use-phase-shifter", + "transmissionCapacities": "transmission-capacities", + "displayComments": "display-comments", + "filterSynthesis": "filter-synthesis", + "filterYearByYear": "filter-year-by-year", + "assetType": "asset-type", + } + + api_link = {mapping.get(k, k): v for k, v in api_link.items()} + api_link["filter-synthesis"] = set(api_link["filter-synthesis"].split(", ")) + api_link["filter-year-by-year"] = set(api_link["filter-year-by-year"].split(", ")) + link_properties = LinkProperties(**api_link) + return Link(link_area_from_id, link_area_to_id, self, link_properties, link_ui) def _join_filter_values_for_json(json_dict: dict, dict_to_extract: dict) -> dict: diff --git a/src/antares/craft/service/local_services/link_local.py b/src/antares/craft/service/local_services/link_local.py index 32963da9..0fefa7b3 100644 --- a/src/antares/craft/service/local_services/link_local.py +++ b/src/antares/craft/service/local_services/link_local.py @@ -122,6 +122,7 @@ def sort_link_properties_dict(ini_dict: Dict[str, str]) -> Dict[str, str]: "display-comments", "filter-synthesis", "filter-year-by-year", + "comments", ] return dict(sorted(ini_dict.items(), key=lambda item: dict_order.index(item[0]))) diff --git a/tests/antares/integration/conftest.py b/tests/antares/integration/conftest.py index 5b9d9b52..cbaf13f6 100644 --- a/tests/antares/integration/conftest.py +++ b/tests/antares/integration/conftest.py @@ -12,9 +12,8 @@ import pytest -from antares.craft import create_study_local from antares.craft.model.area import Area -from antares.craft.model.study import Study +from antares.craft.model.study import Study, create_study_local @pytest.fixture diff --git a/tests/antares/integration/test_local_client.py b/tests/antares/integration/test_local_client.py index 9eac428c..84aed6c0 100644 --- a/tests/antares/integration/test_local_client.py +++ b/tests/antares/integration/test_local_client.py @@ -14,7 +14,6 @@ import numpy as np import pandas as pd -from antares.craft import create_study_local from antares.craft.exceptions.exceptions import AreaCreationError, LinkCreationError from antares.craft.model.area import AdequacyPatchMode, Area, AreaProperties, AreaUi from antares.craft.model.binding_constraint import BindingConstraintProperties, ClusterData, ConstraintTerm, LinkData @@ -25,7 +24,7 @@ from antares.craft.model.settings.playlist_parameters import PlaylistParameters from antares.craft.model.settings.study_settings import StudySettingsLocal from antares.craft.model.st_storage import STStorageGroup, STStorageProperties -from antares.craft.model.study import Study +from antares.craft.model.study import Study, create_study_local from antares.craft.model.thermal import ThermalCluster, ThermalClusterGroup, ThermalClusterProperties from antares.craft.tools.ini_tool import IniFile, IniFileTypes diff --git a/tests/antares/services/api_services/test_link_api.py b/tests/antares/services/api_services/test_link_api.py index fe1e3b1e..bebf2768 100644 --- a/tests/antares/services/api_services/test_link_api.py +++ b/tests/antares/services/api_services/test_link_api.py @@ -19,6 +19,7 @@ from antares.craft.exceptions.exceptions import ( LinkDownloadError, LinkPropertiesUpdateError, + LinksRetrievalError, LinkUiUpdateError, LinkUploadError, ) @@ -29,6 +30,38 @@ from antares.craft.service.service_factory import ServiceFactory +@pytest.fixture() +def expected_link(): + area_from_name = "zone1 auto" + area_to_name = "zone4auto" + api = APIconf("https://antares.com", "token", verify=False) + study_id = "22c52f44-4c2a-407b-862b-490887f93dd8" + link_service = ServiceFactory(api, study_id).create_link_service() + properties = { + "hurdles-cost": False, + "loop-flow": False, + "use-phase-shifter": False, + "transmission-capacities": "enabled", + "asset-type": "ac", + "display-comments": True, + "colorr": 112, + "colorb": 112, + "colorg": 112, + "linkWidth": 1, + "linkStyle": "plain", + "filter-synthesis": set("hourly, daily, weekly, monthly, annual".split(", ")), + "filter-year-by-year": set("hourly, daily, weekly, monthly, annual".split(", ")), + } + color_r = properties.pop("colorr") + color_b = properties.pop("colorb") + color_g = properties.pop("colorg") + link_width = properties.pop("linkWidth") + link_style = properties.pop("linkStyle") + link_properties = LinkProperties(**properties) + link_ui = LinkUi(colorg=color_g, colorb=color_b, colorr=color_r, link_style=link_style, link_width=link_width) + return Link(area_from_name, area_to_name, link_service, link_properties, link_ui) + + class TestCreateAPI: api = APIconf("https://antares.com", "token", verify=False) study_id = "22c52f44-4c2a-407b-862b-490887f93dd8" @@ -182,7 +215,7 @@ def test_create_indirect_capacity_fail(self): f"input/links/{self.area_from.id}/capacities/{self.area_to.id}_indirect" ) - mocker.post(raw_url, status_code=404) + mocker.post(raw_url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( LinkUploadError, @@ -208,7 +241,7 @@ def test_get_parameters_fail(self): f"input/links/{self.area_from.id}/{self.area_to.id}_parameters" ) - mocker.get(raw_url, status_code=404) + mocker.get(raw_url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( LinkDownloadError, @@ -234,7 +267,7 @@ def test_get_indirect_capacity_fail(self): f"input/links/{self.area_from.id}/capacities/{self.area_to.id}_indirect" ) - mocker.get(raw_url, status_code=404) + mocker.get(raw_url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( LinkDownloadError, @@ -260,10 +293,53 @@ def test_get_direct_capacity_fail(self): f"input/links/{self.area_from.id}/capacities/{self.area_to.id}_direct" ) - mocker.get(raw_url, status_code=404) + mocker.get(raw_url, json={"description": self.antares_web_description_msg}, status_code=404) with pytest.raises( LinkDownloadError, match=f"Could not download directcapacity matrix for link {self.area_from.id}/{self.area_to.id}", ): self.link.get_capacity_direct() + + def test_read_links(self, expected_link): + url_read_links = f"https://antares.com/api/v1/studies/{self.study_id}/links" + + json_links = [ + { + "hurdlesCost": False, + "loopFlow": False, + "usePhaseShifter": False, + "transmissionCapacities": "enabled", + "assetType": "ac", + "displayComments": True, + "colorr": 112, + "colorb": 112, + "colorg": 112, + "linkWidth": 1, + "linkStyle": "plain", + "filterSynthesis": "hourly, daily, weekly, monthly, annual", + "filterYearByYear": "hourly, daily, weekly, monthly, annual", + "area1": "zone1 auto", + "area2": "zone4auto", + } + ] + + with requests_mock.Mocker() as mocker: + mocker.get(url_read_links, json=json_links) + actual_link_list = self.study.read_links() + assert len(actual_link_list) == 1 + actual_link = actual_link_list[0] + assert expected_link.id == actual_link.id + assert expected_link.properties == actual_link.properties + assert expected_link.ui == actual_link.ui + + def test_read_links_fail(self): + with requests_mock.Mocker() as mocker: + raw_url = f"https://antares.com/api/v1/studies/{self.study_id}/links" + mocker.get(raw_url, json={"description": self.antares_web_description_msg}, status_code=404) + + with pytest.raises( + LinksRetrievalError, + match=f"Could not retrieve links from study {self.study_id} : {self.antares_web_description_msg}", + ): + self.study.read_links() diff --git a/tests/antares/services/local_services/test_study.py b/tests/antares/services/local_services/test_study.py index 3ad09a0e..c83e193d 100644 --- a/tests/antares/services/local_services/test_study.py +++ b/tests/antares/services/local_services/test_study.py @@ -1683,6 +1683,7 @@ def test_create_link_sets_ini_content(self, tmp_path, local_study_w_areas): display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = """ @@ -1717,6 +1718,7 @@ def test_created_link_has_default_local_properties(self, tmp_path, local_study_w display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = """ expected_ini = ConfigParser() @@ -1765,6 +1767,7 @@ def test_created_link_has_custom_properties(self, tmp_path, local_study_w_areas) display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = daily, weekly +comments = """ expected_ini = ConfigParser() @@ -1804,6 +1807,7 @@ def test_multiple_links_created_from_same_area(self, tmp_path, local_study_w_are display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = [it] hurdles-cost = false @@ -1819,6 +1823,7 @@ def test_multiple_links_created_from_same_area(self, tmp_path, local_study_w_are display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = """ expected_ini = ConfigParser() @@ -1861,6 +1866,7 @@ def test_multiple_links_created_from_same_area_are_alphabetical(self, tmp_path, display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = [it] hurdles-cost = false @@ -1876,6 +1882,7 @@ def test_multiple_links_created_from_same_area_are_alphabetical(self, tmp_path, display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = """ expected_ini = ConfigParser() @@ -1933,6 +1940,7 @@ def test_created_link_has_default_ui_values(self, tmp_path, local_study_w_areas) display-comments = true filter-synthesis = hourly, daily, weekly, monthly, annual filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = """ expected_ini = ConfigParser() @@ -1970,6 +1978,7 @@ def test_created_link_with_custom_ui_values(self, tmp_path, local_study_w_areas) display-comments = true filter-synthesis = hourly, weekly, monthly filter-year-by-year = hourly, daily, weekly, monthly, annual +comments = """ expected_ini = ConfigParser() diff --git a/tests/integration/antares_web_desktop.py b/tests/integration/antares_web_desktop.py index 834a6eb8..b77af98c 100644 --- a/tests/integration/antares_web_desktop.py +++ b/tests/integration/antares_web_desktop.py @@ -64,10 +64,10 @@ def kill(self): It also kills the AntaresWebDesktop instance. """ session = requests.Session() - res = session.get(self.url + "/v1/studies") + res = session.get(self.url + "/api/v1/studies") studies = res.json() for study in studies: - session.delete(self.url + f"/v1/studies/{study}?children=True") + session.delete(self.url + f"/api/v1/studies/{study}?children=True") self.process.terminate() self.process.wait() pids = subprocess.run(["pgrep AntaresWeb"], capture_output=True, shell=True).stdout.split() diff --git a/tests/integration/test_web_client.py b/tests/integration/test_web_client.py index a707918f..676bc115 100644 --- a/tests/integration/test_web_client.py +++ b/tests/integration/test_web_client.py @@ -399,6 +399,15 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop): thermal_fr.update_properties(new_props) assert thermal_fr.properties.group == ThermalClusterGroup.NUCLEAR + # assert study got all links + links = study.read_links() + assert len(links) == 2 + test_link_be_fr = links[0] + test_link_de_fr = links[1] + assert test_link_be_fr.id == link_be_fr.id + assert test_link_be_fr.properties == link_be_fr.properties + assert test_link_de_fr.id == link_de_fr.id + # tests renewable properties update new_props = RenewableClusterProperties() new_props.ts_interpretation = TimeSeriesInterpretation.POWER_GENERATION