diff --git a/antarest/study/business/link_management.py b/antarest/study/business/link_management.py index 7c6a971fde..7997fb1d1a 100644 --- a/antarest/study/business/link_management.py +++ b/antarest/study/business/link_management.py @@ -11,123 +11,35 @@ # This file is part of the Antares project. import typing as t -from typing import Any -from antares.study.version import StudyVersion - -from antarest.core.exceptions import LinkNotFound from antarest.core.model import JSON -from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO, LinkInternal -from antarest.study.business.utils import execute_or_add_commands +from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO +from antarest.study.dao.dao_factory import DAOFactory from antarest.study.model import RawStudy, Study -from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy -from antarest.study.storage.storage_service import StudyStorageService -from antarest.study.storage.variantstudy.model.command.create_link import CreateLink -from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink -from antarest.study.storage.variantstudy.model.command.update_link import UpdateLink class LinkManager: - def __init__(self, storage_service: StudyStorageService) -> None: - self.storage_service = storage_service + def __init__(self, dao_factory: DAOFactory) -> None: + self.composite_dao = dao_factory.create_link_dao() def get_all_links(self, study: Study) -> t.List[LinkDTO]: - file_study = self.storage_service.get_storage(study).get_raw(study) - result: t.List[LinkDTO] = [] - - for area_id, area in file_study.config.areas.items(): - links_config = file_study.tree.get(["input", "links", area_id, "properties"]) - - for link in area.links: - link_tree_config: t.Dict[str, t.Any] = links_config[link] - link_tree_config.update({"area1": area_id, "area2": link}) - - link_internal = LinkInternal.model_validate(link_tree_config) - - result.append(link_internal.to_dto()) - - return result - - def get_link(self, study: RawStudy, link: LinkInternal) -> LinkInternal: - file_study = self.storage_service.get_storage(study).get_raw(study) - - link_properties = self._get_link_if_exists(file_study, link) - - link_properties.update({"area1": link.area1, "area2": link.area2}) - - updated_link = LinkInternal.model_validate(link_properties) - - return updated_link + return self.composite_dao.get_links(study) def create_link(self, study: Study, link_creation_dto: LinkDTO) -> LinkDTO: - link = link_creation_dto.to_internal(StudyVersion.parse(study.version)) - - storage_service = self.storage_service.get_storage(study) - file_study = storage_service.get_raw(study) - - command = CreateLink( - area1=link.area1, - area2=link.area2, - parameters=link.model_dump(exclude_none=True), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ) - - execute_or_add_commands(study, file_study, [command], self.storage_service) - - return link_creation_dto + return self.composite_dao.create_link(study, link_creation_dto) def update_link(self, study: RawStudy, area_from: str, area_to: str, link_update_dto: LinkBaseDTO) -> LinkDTO: - link_dto = LinkDTO(area1=area_from, area2=area_to, **link_update_dto.model_dump(exclude_unset=True)) - - link = link_dto.to_internal(StudyVersion.parse(study.version)) - file_study = self.storage_service.get_storage(study).get_raw(study) - - self._get_link_if_exists(file_study, link) - - command = UpdateLink( - area1=link.area1, - area2=link.area2, - parameters=link.model_dump( - include=link_update_dto.model_fields_set, exclude={"area1", "area2"}, exclude_none=True - ), - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ) - - execute_or_add_commands(study, file_study, [command], self.storage_service) - - updated_link = self.get_link(study, link) - - return updated_link.to_dto() + return self.composite_dao.update_link(study, area_from, area_to, link_update_dto) def update_links( self, study: RawStudy, update_links_by_ids: t.Mapping[t.Tuple[str, str], LinkBaseDTO], ) -> t.Mapping[t.Tuple[str, str], LinkBaseDTO]: - new_links_by_ids = {} - for (area1, area2), update_link_dto in update_links_by_ids.items(): - updated_link = self.update_link(study, area1, area2, update_link_dto) - new_links_by_ids[(area1, area2)] = updated_link - - return new_links_by_ids + return self.composite_dao.update_links(study, update_links_by_ids) def delete_link(self, study: RawStudy, area1_id: str, area2_id: str) -> None: - file_study = self.storage_service.get_storage(study).get_raw(study) - command = RemoveLink( - area1=area1_id, - area2=area2_id, - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ) - execute_or_add_commands(study, file_study, [command], self.storage_service) - - def _get_link_if_exists(self, file_study: FileStudy, link: LinkInternal) -> dict[str, Any]: - try: - return file_study.tree.get(["input", "links", link.area1, "properties", link.area2]) - except KeyError: - raise LinkNotFound(f"The link {link.area1} -> {link.area2} is not present in the study") + self.composite_dao.delete_link(study, area1_id, area2_id) @staticmethod def get_table_schema() -> JSON: diff --git a/antarest/study/dao/__init__.py b/antarest/study/dao/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/antarest/study/dao/dao_factory.py b/antarest/study/dao/dao_factory.py new file mode 100644 index 0000000000..e3c70adcd0 --- /dev/null +++ b/antarest/study/dao/dao_factory.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from antarest.core.interfaces.cache import ICache +from antarest.study.dao.link.composite_link_dao import CompositeLinkDAO +from antarest.study.storage.storage_service import StudyStorageService + + +class DAOFactory: + def __init__(self, storage_service: StudyStorageService, cache_service: ICache): + self.storage_service = storage_service + self.cache_service = cache_service + + def create_link_dao(self) -> CompositeLinkDAO: + return CompositeLinkDAO(self.storage_service, self.cache_service) diff --git a/antarest/study/dao/link/__init__.py b/antarest/study/dao/link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/antarest/study/dao/link/cache_link_dao.py b/antarest/study/dao/link/cache_link_dao.py new file mode 100644 index 0000000000..7e5dc22417 --- /dev/null +++ b/antarest/study/dao/link/cache_link_dao.py @@ -0,0 +1,58 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +import typing as t + +from antarest.core.interfaces.cache import ICache +from antarest.study.business.model.link_model import LinkDTO + + +class CacheLinkDao: + def __init__(self, cache: ICache): + self.cache = cache + + def get_links(self, study_id: str) -> t.Optional[t.List[LinkDTO]]: + cache_key = self._get_cache_key(study_id) + cached_data = self.cache.get(cache_key) + if cached_data is None: + return None + return [LinkDTO.model_validate(link) for link in cached_data["links"]] + + def put(self, study_id: str, link: LinkDTO, timeout: int = 600000) -> None: + try: + cache_key = self._get_cache_key(study_id) + cached_links = self.cache.get(cache_key) + + if cached_links is None: + cached_links = {"links": []} + + existing_links = cached_links.get("links", []) + + if isinstance(existing_links, dict): + existing_links = list(existing_links.values()) + elif not isinstance(existing_links, list): + existing_links = [] + + link_data = link.model_dump() + existing_links.append(link_data) + + cached_links["links"] = existing_links + self.cache.put(cache_key, cached_links, timeout) + except: + raise + + def invalidate(self, study_id: str) -> None: + cache_key = self._get_cache_key(study_id) + self.cache.invalidate(cache_key) + + def _get_cache_key(self, study_id: str) -> str: + return f"links:{study_id}" diff --git a/antarest/study/dao/link/composite_link_dao.py b/antarest/study/dao/link/composite_link_dao.py new file mode 100644 index 0000000000..113425f367 --- /dev/null +++ b/antarest/study/dao/link/composite_link_dao.py @@ -0,0 +1,57 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +import typing as t + +from antarest.core.interfaces.cache import ICache +from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO +from antarest.study.dao.link.cache_link_dao import CacheLinkDao +from antarest.study.dao.link.storage_link_dao import StorageLinkDao +from antarest.study.model import Study +from antarest.study.storage.storage_service import StudyStorageService + + +class CompositeLinkDAO: + def __init__(self, storage_service: StudyStorageService, cache_service: ICache): + self.storage_dao = StorageLinkDao(storage_service) + self.cache_dao = CacheLinkDao(cache_service) + + def create_link(self, study: Study, link: LinkDTO) -> LinkDTO: + self.cache_dao.invalidate(study.id) + return self.storage_dao.create_link(study, link) + + def get_links(self, study: Study) -> t.List[LinkDTO]: + links = self.cache_dao.get_links(study.id) + if links is not None: + return links + + links = self.storage_dao.get_links(study) + + for link in links: + self.cache_dao.put(study.id, link) + return links + + def update_link(self, study: Study, area_from: str, area_to: str, link_update_dto: LinkBaseDTO) -> LinkDTO: + self.cache_dao.invalidate(study.id) + return self.storage_dao.update_link(study, area_from, area_to, link_update_dto) + + def update_links( + self, + study: Study, + update_links_by_ids: t.Mapping[t.Tuple[str, str], LinkBaseDTO], + ) -> t.Mapping[t.Tuple[str, str], LinkBaseDTO]: + self.cache_dao.invalidate(study.id) + return self.storage_dao.update_links(study, update_links_by_ids) + + def delete_link(self, study: Study, area_from: str, area_to: str) -> None: + self.cache_dao.invalidate(study.id) + self.storage_dao.delete_link(study, area_from, area_to) diff --git a/antarest/study/dao/link/storage_link_dao.py b/antarest/study/dao/link/storage_link_dao.py new file mode 100644 index 0000000000..e0b6bf406d --- /dev/null +++ b/antarest/study/dao/link/storage_link_dao.py @@ -0,0 +1,127 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +import typing as t + +from antares.study.version import StudyVersion + +from antarest.core.exceptions import LinkNotFound +from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO, LinkInternal +from antarest.study.business.utils import execute_or_add_commands +from antarest.study.model import Study +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.storage_service import StudyStorageService +from antarest.study.storage.variantstudy.model.command.create_link import CreateLink +from antarest.study.storage.variantstudy.model.command.remove_link import RemoveLink +from antarest.study.storage.variantstudy.model.command.update_link import UpdateLink + + +class StorageLinkDao: + def __init__(self, storage_service: StudyStorageService): + self.storage_service = storage_service + + def create_link(self, study: Study, link_dto: LinkDTO) -> LinkDTO: + link = link_dto.to_internal(StudyVersion.parse(study.version)) + + storage_service = self.storage_service.get_storage(study) + file_study = storage_service.get_raw(study) + + command = CreateLink( + area1=link.area1, + area2=link.area2, + parameters=link.model_dump(exclude_none=True), + command_context=self.storage_service.variant_study_service.command_factory.command_context, + study_version=file_study.config.version, + ) + + execute_or_add_commands(study, file_study, [command], self.storage_service) + + return link_dto + + def get_links(self, study: Study) -> t.List[LinkDTO]: + file_study = self.storage_service.get_storage(study).get_raw(study) + result: t.List[LinkDTO] = [] + + for area_id, area in file_study.config.areas.items(): + links_config = file_study.tree.get(["input", "links", area_id, "properties"]) + + for link in area.links: + link_tree_config: t.Dict[str, t.Any] = links_config[link] + link_tree_config.update({"area1": area_id, "area2": link}) + + link_internal = LinkInternal.model_validate(link_tree_config) + + result.append(link_internal.to_dto()) + + return result + + def update_link(self, study: Study, area_from: str, area_to: str, link_update_dto: LinkBaseDTO) -> LinkDTO: + link_dto = LinkDTO(area1=area_from, area2=area_to, **link_update_dto.model_dump(exclude_unset=True)) + + link = link_dto.to_internal(StudyVersion.parse(study.version)) + file_study = self.storage_service.get_storage(study).get_raw(study) + + self._get_link_if_exists(file_study, link) + + command = UpdateLink( + area1=link.area1, + area2=link.area2, + parameters=link.model_dump( + include=link_update_dto.model_fields_set, exclude={"area1", "area2"}, exclude_none=True + ), + command_context=self.storage_service.variant_study_service.command_factory.command_context, + study_version=file_study.config.version, + ) + + execute_or_add_commands(study, file_study, [command], self.storage_service) + + updated_link = self._get_updated_link(study, link) + + return updated_link.to_dto() + + def update_links( + self, + study: Study, + update_links_by_ids: t.Mapping[t.Tuple[str, str], LinkBaseDTO], + ) -> t.Mapping[t.Tuple[str, str], LinkBaseDTO]: + new_links_by_ids = {} + for (area1, area2), update_link_dto in update_links_by_ids.items(): + updated_link = self.update_link(study, area1, area2, update_link_dto) + new_links_by_ids[(area1, area2)] = updated_link + + return new_links_by_ids + + def delete_link(self, study: Study, area1_id: str, area2_id: str) -> None: + file_study = self.storage_service.get_storage(study).get_raw(study) + command = RemoveLink( + area1=area1_id, + area2=area2_id, + command_context=self.storage_service.variant_study_service.command_factory.command_context, + study_version=file_study.config.version, + ) + execute_or_add_commands(study, file_study, [command], self.storage_service) + + def _get_updated_link(self, study: Study, link: LinkInternal) -> LinkInternal: + file_study = self.storage_service.get_storage(study).get_raw(study) + + link_properties = self._get_link_if_exists(file_study, link) + + link_properties.update({"area1": link.area1, "area2": link.area2}) + + updated_link = LinkInternal.model_validate(link_properties) + + return updated_link + + def _get_link_if_exists(self, file_study: FileStudy, link: LinkInternal) -> dict[str, t.Any]: + try: + return file_study.tree.get(["input", "links", link.area1, "properties", link.area2]) + except KeyError: + raise LinkNotFound(f"The link {link.area1} -> {link.area2} is not present in the study") diff --git a/antarest/study/service.py b/antarest/study/service.py index 3e198addb3..f69c7eace4 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -104,6 +104,7 @@ XpansionCandidateDTO, XpansionManager, ) +from antarest.study.dao.dao_factory import DAOFactory from antarest.study.model import ( DEFAULT_WORKSPACE_NAME, NEW_DEFAULT_STUDY_VERSION, @@ -379,6 +380,7 @@ def __init__( config: Config, ): self.storage_service = StudyStorageService(raw_study_service, variant_study_service) + self.dao_factory = DAOFactory(self.storage_service, cache_service) self.user_service = user_service self.repository = repository self.event_bus = event_bus @@ -386,7 +388,7 @@ def __init__( self.task_service = task_service self.areas = AreaManager(self.storage_service, self.repository) self.district_manager = DistrictManager(self.storage_service) - self.links_manager = LinkManager(self.storage_service) + self.links_manager = LinkManager(self.dao_factory) self.config_manager = ConfigManager(self.storage_service) self.general_manager = GeneralManager(self.storage_service) self.thematic_trimming_manager = ThematicTrimmingManager(self.storage_service) @@ -2066,7 +2068,7 @@ def delete_link( ) if referencing_binding_constraints: binding_ids = [bc.id for bc in referencing_binding_constraints] - raise ReferencedObjectDeletionNotAllowed(link_id, binding_ids, object_type="Link") + raise ReferencedObjectDeletionNotAllowed(link_id, binding_ids, object_type="link") self.links_manager.delete_link(study, area_from, area_to) self.event_bus.push( Event( diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index b61074b8b7..bb243c3494 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -18,6 +18,7 @@ import pytest +from antarest.core.interfaces.cache import ICache from antarest.core.jwt import DEFAULT_ADMIN_USER from antarest.core.requests import RequestParameters from antarest.core.utils.fastapi_sqlalchemy import db @@ -26,6 +27,7 @@ from antarest.study.business.area_management import AreaCreationDTO, AreaManager, AreaType, UpdateAreaUi from antarest.study.business.link_management import LinkDTO, LinkManager from antarest.study.business.model.link_model import AssetType, LinkStyle, TransmissionCapacity +from antarest.study.dao.dao_factory import DAOFactory from antarest.study.model import Patch, PatchArea, PatchCluster, RawStudy, StudyAdditionalData from antarest.study.repository import StudyMetadataRepository from antarest.study.storage.patch_service import PatchService @@ -96,7 +98,8 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): storage_service=storage_service, repository=StudyMetadataRepository(Mock()), ) - link_manager = LinkManager(storage_service=storage_service) + dao_factory = DAOFactory(storage_service, ICache()) + link_manager = LinkManager(dao_factory) # Check `AreaManager` behaviour with a RAW study study_id = str(uuid.uuid4()) @@ -323,11 +326,11 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): def test_get_all_area(): raw_study_service = Mock(spec=RawStudyService) + study_storage_service = StudyStorageService(raw_study_service, Mock()) area_manager = AreaManager( - storage_service=StudyStorageService(raw_study_service, Mock()), + storage_service=study_storage_service, repository=Mock(spec=StudyMetadataRepository), ) - link_manager = LinkManager(storage_service=StudyStorageService(raw_study_service, Mock())) study = RawStudy(version="900") config = FileStudyTreeConfig( @@ -533,6 +536,8 @@ def test_get_all_area(): } }, ] + dao_factory = DAOFactory(study_storage_service, ICache()) + link_manager = LinkManager(dao_factory) links = link_manager.get_all_links(study) assert [ { diff --git a/tests/variantstudy/model/command/test_create_link.py b/tests/variantstudy/model/command/test_create_link.py index 611c2918e1..a50c6f8a2f 100644 --- a/tests/variantstudy/model/command/test_create_link.py +++ b/tests/variantstudy/model/command/test_create_link.py @@ -10,7 +10,6 @@ # # This file is part of the Antares project. -import configparser from unittest.mock import Mock import numpy as np @@ -18,7 +17,7 @@ from pydantic import ValidationError from antarest.core.exceptions import LinkValidationError -from antarest.study.business.link_management import LinkInternal +from antarest.study.business.model.link_model import LinkInternal from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.rawstudy.ini_reader import IniReader from antarest.study.storage.rawstudy.model.filesystem.config.field_validators import transform_name_to_id