diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 7fe58c346b..484e0827d5 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -11,6 +11,7 @@ # This file is part of the Antares project. import logging +import re import typing as t from antarest.core.exceptions import ConfigFileNotFound, DuplicateAreaName, LayerNotAllowedToBeDeleted, LayerNotFound @@ -23,14 +24,16 @@ ClusterInfoDTO, LayerInfoDTO, UpdateAreaUi, - _get_area_layers, - _get_ui_info_map, ) from antarest.study.business.utils import execute_or_add_commands from antarest.study.model import Patch, PatchArea, PatchCluster, RawStudy, Study from antarest.study.repository import StudyMetadataRepository from antarest.study.storage.patch_service import PatchService -from antarest.study.storage.rawstudy.model.filesystem.config.area import AreaFolder, ThermalAreasProperties +from antarest.study.storage.rawstudy.model.filesystem.config.area import ( + AreaFolder, + ThermalAreasProperties, + UIProperties, +) from antarest.study.storage.rawstudy.model.filesystem.config.model import Area, DistrictSet, transform_name_to_id from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.storage_service import StudyStorageService @@ -47,6 +50,47 @@ _THERMAL_AREAS_PATH = "input/thermal/areas" +def _get_ui_info_map(file_study: FileStudy, area_ids: t.Sequence[str]) -> t.Dict[str, t.Any]: + """ + Get the UI information (a JSON object) for each selected Area. + + Args: + file_study: A file study from which the configuration can be read. + area_ids: List of selected area IDs. + + Returns: + Dictionary where keys are IDs, and values are UI objects. + + Raises: + ChildNotFoundError: if one of the Area IDs is not found in the configuration. + """ + # If there is no ID, it is better to return an empty dictionary + # instead of raising an obscure exception. + if not area_ids: + return {} + + ui_info_map = file_study.tree.get(["input", "areas", ",".join(area_ids), "ui"]) + + # If there is only one ID in the `area_ids`, the result returned from + # the `file_study.tree.get` call will be a single UI object. + # On the other hand, if there are multiple values in `area_ids`, + # the result will be a dictionary where the keys are the IDs, + # and the values are the corresponding UI objects. + if len(area_ids) == 1: + ui_info_map = {area_ids[0]: ui_info_map} + + # Convert to UIProperties to ensure that the UI object is valid. + ui_info_map = {area_id: UIProperties(**ui_info).to_config() for area_id, ui_info in ui_info_map.items()} + + return ui_info_map + + +def _get_area_layers(area_uis: t.Dict[str, t.Any], area: str) -> t.List[str]: + if area in area_uis and "ui" in area_uis[area] and "layers" in area_uis[area]["ui"]: + return re.split(r"\s+", (str(area_uis[area]["ui"]["layers"]) or "")) + return [] + + class AreaManager: """ Manages operations related to areas in a study, including retrieval, creation, and updates. @@ -195,7 +239,7 @@ def update_areas_props( @staticmethod def get_table_schema() -> JSON: - return AreaOutput.schema() + return AreaOutput.model_json_schema() def get_all_areas(self, study: RawStudy, area_type: t.Optional[AreaType] = None) -> t.List[AreaInfoDTO]: """ diff --git a/antarest/study/business/model/area_model.py b/antarest/study/business/model/area_model.py index b304286d16..c1357e66fa 100644 --- a/antarest/study/business/model/area_model.py +++ b/antarest/study/business/model/area_model.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. - +True import enum import re import typing as t @@ -69,44 +69,6 @@ class LayerInfoDTO(AntaresBaseModel): class UpdateAreaUi(AntaresBaseModel, extra="forbid", populate_by_name=True): - """ - DTO for updating area UI - - Usage: - - >>> from antarest.study.business.model.area_model import UpdateAreaUi - >>> from pprint import pprint - - >>> obj = { - ... "x": -673.75, - ... "y": 301.5, - ... "color_rgb": [230, 108, 44], - ... "layerX": {"0": -230, "4": -230, "6": -95, "7": -230, "8": -230}, - ... "layerY": {"0": 136, "4": 136, "6": 39, "7": 136, "8": 136}, - ... "layerColor": { - ... "0": "230, 108, 44", - ... "4": "230, 108, 44", - ... "6": "230, 108, 44", - ... "7": "230, 108, 44", - ... "8": "230, 108, 44", - ... }, - ... } - - >>> model = UpdateAreaUi(**obj) - >>> pprint(model.model_dump(by_alias=True), width=80) - {'colorRgb': [230, 108, 44], - 'layerColor': {0: '230, 108, 44', - 4: '230, 108, 44', - 6: '230, 108, 44', - 7: '230, 108, 44', - 8: '230, 108, 44'}, - 'layerX': {0: -230, 4: -230, 6: -95, 7: -230, 8: -230}, - 'layerY': {0: 136, 4: 136, 6: 39, 7: 136, 8: 136}, - 'x': -673, - 'y': 301} - - """ - x: int = Field(title="X position") y: int = Field(title="Y position") color_rgb: t.Sequence[int] = Field(title="RGB color", alias="colorRgb") @@ -115,47 +77,6 @@ class UpdateAreaUi(AntaresBaseModel, extra="forbid", populate_by_name=True): layer_color: t.Mapping[int, str] = Field(default_factory=dict, title="Color of each layer", alias="layerColor") -def _get_ui_info_map(file_study: FileStudy, area_ids: t.Sequence[str]) -> t.Dict[str, t.Any]: - """ - Get the UI information (a JSON object) for each selected Area. - - Args: - file_study: A file study from which the configuration can be read. - area_ids: List of selected area IDs. - - Returns: - Dictionary where keys are IDs, and values are UI objects. - - Raises: - ChildNotFoundError: if one of the Area IDs is not found in the configuration. - """ - # If there is no ID, it is better to return an empty dictionary - # instead of raising an obscure exception. - if not area_ids: - return {} - - ui_info_map = file_study.tree.get(["input", "areas", ",".join(area_ids), "ui"]) - - # If there is only one ID in the `area_ids`, the result returned from - # the `file_study.tree.get` call will be a single UI object. - # On the other hand, if there are multiple values in `area_ids`, - # the result will be a dictionary where the keys are the IDs, - # and the values are the corresponding UI objects. - if len(area_ids) == 1: - ui_info_map = {area_ids[0]: ui_info_map} - - # Convert to UIProperties to ensure that the UI object is valid. - ui_info_map = {area_id: UIProperties(**ui_info).to_config() for area_id, ui_info in ui_info_map.items()} - - return ui_info_map - - -def _get_area_layers(area_uis: t.Dict[str, t.Any], area: str) -> t.List[str]: - if area in area_uis and "ui" in area_uis[area] and "layers" in area_uis[area]["ui"]: - return re.split(r"\s+", (str(area_uis[area]["ui"]["layers"]) or "")) - return [] - - # noinspection SpellCheckingInspection class _BaseAreaDTO( OptimizationProperties.FilteringSection, diff --git a/tests/integration/study_data_blueprint/test_area.py b/tests/integration/study_data_blueprint/test_area.py new file mode 100644 index 0000000000..e1e81a35ee --- /dev/null +++ b/tests/integration/study_data_blueprint/test_area.py @@ -0,0 +1,79 @@ +# Copyright (c) 2025, 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 pytest +from starlette.testclient import TestClient + +from tests.integration.prepare_proxy import PreparerProxy + + +@pytest.mark.unit_test +class TestArea: + @pytest.mark.parametrize("study_type", ["raw", "variant"]) + def test_area(self, client: TestClient, user_access_token: str, study_type: str) -> None: + client.headers = {"Authorization": f"Bearer {user_access_token}"} # type: ignore + + preparer = PreparerProxy(client, user_access_token) + study_id = preparer.create_study("foo", version=820) + if study_type == "variant": + study_id = preparer.create_variant(study_id, name="Variant 1") + + client.post(f"/v1/studies/{study_id}/areas", json={"name": "area1", "type": "AREA"}) + client.post(f"/v1/studies/{study_id}/areas", json={"name": "area2", "type": "AREA"}) + + res = client.get(f"/v1/studies/{study_id}/areas?ui=true") + assert res.status_code == 200 + expected = { + "area1": { + "layerColor": {"0": "230, 108, 44"}, + "layerX": {"0": 0}, + "layerY": {"0": 0}, + "ui": {"color_b": 44, "color_g": 108, "color_r": 230, "layers": "0", "x": 0, "y": 0}, + }, + "area2": { + "layerColor": {"0": "230, 108, 44"}, + "layerX": {"0": 0}, + "layerY": {"0": 0}, + "ui": {"color_b": 44, "color_g": 108, "color_r": 230, "layers": "0", "x": 0, "y": 0}, + }, + } + assert res.json() == expected + + client.put( + f"/v1/studies/{study_id}/areas/area1/ui", + json={ + "x": 10, + "y": 10, + "layerColor": {"0": "100, 100, 100"}, + "layerX": {"0": 10}, + "layerY": {"0": 10}, + "color_rgb": (100, 100, 100), + }, + ) + + res = client.get(f"/v1/studies/{study_id}/areas?ui=true") + assert res.status_code == 200 + expected = { + "area1": { + "layerColor": {"0": "100, 100, 100"}, + "layerX": {"0": 10}, + "layerY": {"0": 10}, + "ui": {"color_b": 100, "color_g": 100, "color_r": 100, "layers": "0", "x": 10, "y": 10}, + }, + "area2": { + "layerColor": {"0": "230, 108, 44"}, + "layerX": {"0": 0}, + "layerY": {"0": 0}, + "ui": {"color_b": 44, "color_g": 108, "color_r": 230, "layers": "0", "x": 0, "y": 0}, + }, + } + assert res.json() == expected