From 69febeeb034f2ab00607950f41e9914a1bb1c884 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Tue, 28 Jan 2025 13:58:13 +0100 Subject: [PATCH 1/9] fix(area): rename area service --- antarest/study/service.py | 18 ++++++++++-------- antarest/study/web/study_data_blueprint.py | 10 +++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/antarest/study/service.py b/antarest/study/service.py index 31bb80fa1f..0655e67a4e 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -403,7 +403,7 @@ def __init__( self.event_bus = event_bus self.file_transfer_manager = file_transfer_manager self.task_service = task_service - self.areas = AreaManager(self.storage_service, self.repository) + self.area_manager = AreaManager(self.storage_service, self.repository) self.district_manager = DistrictManager(self.storage_service) self.links_manager = LinkManager(self.storage_service) self.config_manager = ConfigManager(self.storage_service) @@ -426,7 +426,7 @@ def __init__( self.binding_constraint_manager = BindingConstraintManager(self.storage_service) self.correlation_manager = CorrelationManager(self.storage_service) self.table_mode_manager = TableModeManager( - self.areas, + self.area_manager, self.links_manager, self.thermal_manager, self.renewable_manager, @@ -1899,7 +1899,9 @@ def get_all_areas( ) -> t.Union[t.List[AreaInfoDTO], t.Dict[str, t.Any]]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - return self.areas.get_all_areas_ui_info(study) if ui else self.areas.get_all_areas(study, area_type) + return ( + self.area_manager.get_all_areas_ui_info(study) if ui else self.area_manager.get_all_areas(study, area_type) + ) def get_all_links( self, @@ -1919,7 +1921,7 @@ def create_area( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - new_area = self.areas.create_area(study, area_creation_dto) + new_area = self.area_manager.create_area(study, area_creation_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -1979,7 +1981,7 @@ def update_area( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - updated_area = self.areas.update_area_metadata(study, area_id, area_patch_dto) + updated_area = self.area_manager.update_area_metadata(study, area_id, area_patch_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -2000,7 +2002,7 @@ def update_area_ui( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.areas.update_area_ui(study, area_id, area_ui, layer) + return self.area_manager.update_area_ui(study, area_id, area_ui, layer) def update_thermal_cluster_metadata( self, @@ -2012,7 +2014,7 @@ def update_thermal_cluster_metadata( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.areas.update_thermal_cluster_metadata(study, area_id, clusters_metadata) + return self.area_manager.update_thermal_cluster_metadata(study, area_id, clusters_metadata) def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> None: """ @@ -2036,7 +2038,7 @@ def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> Non if referencing_binding_constraints: binding_ids = [bc.id for bc in referencing_binding_constraints] raise ReferencedObjectDeletionNotAllowed(area_id, binding_ids, object_type="Area") - self.areas.delete_area(study, area_id) + self.area_manager.delete_area(study, area_id) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 95659ecd7f..5b8064328a 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -323,7 +323,7 @@ def get_layers( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.areas.get_layers(study) + return study_service.area_manager.get_layers(study) @bp.post( "/studies/{uuid}/layers", @@ -342,7 +342,7 @@ def create_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.areas.create_layer(study, name) + return study_service.area_manager.create_layer(study, name) @bp.put( "/studies/{uuid}/layers/{layer_id}", @@ -363,9 +363,9 @@ def update_layer( params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) if name: - study_service.areas.update_layer_name(study, layer_id, name) + study_service.area_manager.update_layer_name(study, layer_id, name) if areas: - study_service.areas.update_layer_areas(study, layer_id, areas) + study_service.area_manager.update_layer_areas(study, layer_id, areas) @bp.delete( "/studies/{uuid}/layers/{layer_id}", @@ -385,7 +385,7 @@ def remove_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - study_service.areas.remove_layer(study, layer_id) + study_service.area_manager.remove_layer(study, layer_id) @bp.get( "/studies/{uuid}/districts", From 0636b9bb31595cd45c22c041e0ff4ed7aaf85e02 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Wed, 29 Jan 2025 11:58:33 +0100 Subject: [PATCH 2/9] feat(area): add a move_area command --- antarest/study/business/area_management.py | 75 ++------------- .../variantstudy/business/command_reverter.py | 5 + .../storage/variantstudy/command_factory.py | 2 + .../variantstudy/model/command/common.py | 1 + .../variantstudy/model/command/move_area.py | 96 +++++++++++++++++++ .../storage/business/test_arealink_manager.py | 46 +++------ tests/variantstudy/test_command_factory.py | 5 + 7 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 antarest/study/storage/variantstudy/model/command/move_area.py diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index c607be980d..1871c876ac 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -37,6 +37,7 @@ from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.move_area import MoveArea from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig @@ -680,73 +681,17 @@ def update_area_metadata( ) def update_area_ui(self, study: Study, area_id: str, area_ui: UpdateAreaUi, layer: str = "0") -> None: - obj = { - "x": area_ui.x, - "y": area_ui.y, - "color_r": area_ui.color_rgb[0], - "color_g": area_ui.color_rgb[1], - "color_b": area_ui.color_rgb[2], - } file_study = self.storage_service.get_storage(study).get_raw(study) - commands = ( - [ - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/x", - data=obj["x"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/y", - data=obj["y"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/color_r", - data=obj["color_r"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/color_g", - data=obj["color_g"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/color_b", - data=obj["color_b"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - ] - if layer == "0" - else [] - ) - commands.extend( - [ - UpdateConfig( - target=f"input/areas/{area_id}/ui/layerX/{layer}", - data=obj["x"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/layerY/{layer}", - data=obj["y"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/layerColor/{layer}", - data=f"{obj['color_r']},{obj['color_g']},{obj['color_b']}", - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - ] + + command = MoveArea( + area_name=area_id, + new_area_parameters=area_ui.model_dump(), + layer=layer, + 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, commands, self.storage_service) + + execute_or_add_commands(study, file_study, [command], self.storage_service) def update_thermal_cluster_metadata( self, diff --git a/antarest/study/storage/variantstudy/business/command_reverter.py b/antarest/study/storage/variantstudy/business/command_reverter.py index 4b74d2a0ba..20243375e0 100644 --- a/antarest/study/storage/variantstudy/business/command_reverter.py +++ b/antarest/study/storage/variantstudy/business/command_reverter.py @@ -33,6 +33,7 @@ GenerateThermalClusterTimeSeries, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.move_area import MoveArea from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.remove_binding_constraint import RemoveBindingConstraint from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster @@ -72,6 +73,10 @@ def _revert_create_area(base_command: CreateArea, history: t.List["ICommand"], b def _revert_remove_area(base_command: RemoveArea, history: t.List["ICommand"], base: FileStudy) -> t.List[ICommand]: raise NotImplementedError("The revert function for RemoveArea is not available") + @staticmethod + def _revert_move_area(base_command: MoveArea, history: t.List["ICommand"], base: FileStudy) -> t.List[ICommand]: + raise NotImplementedError("The revert function for MoveArea is not available") + @staticmethod def _revert_create_district( base_command: CreateDistrict, diff --git a/antarest/study/storage/variantstudy/command_factory.py b/antarest/study/storage/variantstudy/command_factory.py index f20f0ef58d..8696f0f547 100644 --- a/antarest/study/storage/variantstudy/command_factory.py +++ b/antarest/study/storage/variantstudy/command_factory.py @@ -32,6 +32,7 @@ GenerateThermalClusterTimeSeries, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.move_area import MoveArea from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.remove_binding_constraint import RemoveBindingConstraint from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster @@ -57,6 +58,7 @@ COMMAND_MAPPING = { CommandName.CREATE_AREA.value: CreateArea, + CommandName.MOVE_AREA.value: MoveArea, CommandName.REMOVE_AREA.value: RemoveArea, CommandName.CREATE_DISTRICT.value: CreateDistrict, CommandName.REMOVE_DISTRICT.value: RemoveDistrict, diff --git a/antarest/study/storage/variantstudy/model/command/common.py b/antarest/study/storage/variantstudy/model/command/common.py index e91a0e2e58..c75bee0d47 100644 --- a/antarest/study/storage/variantstudy/model/command/common.py +++ b/antarest/study/storage/variantstudy/model/command/common.py @@ -30,6 +30,7 @@ class FilteringOptions: class CommandName(Enum): CREATE_AREA = "create_area" + MOVE_AREA = "move_area" REMOVE_AREA = "remove_area" CREATE_DISTRICT = "create_district" REMOVE_DISTRICT = "remove_district" diff --git a/antarest/study/storage/variantstudy/model/command/move_area.py b/antarest/study/storage/variantstudy/model/command/move_area.py new file mode 100644 index 0000000000..08ab6f5b1f --- /dev/null +++ b/antarest/study/storage/variantstudy/model/command/move_area.py @@ -0,0 +1,96 @@ +# 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 typing as t + +from typing_extensions import override + +from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput +from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand +from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener +from antarest.study.storage.variantstudy.model.model import CommandDTO + + +class MoveArea(ICommand): + """ + Command used to move an area in the study. + """ + + # Overloaded metadata + # =================== + + command_name: CommandName = CommandName.MOVE_AREA + version: int = 1 + + # Command parameters + # ================== + + area_name: str + new_area_parameters: t.Dict[str, t.Any] + layer: str + + @override + def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: + return ( + CommandOutput( + status=True, + message=f"area '{self.area_name}' updated", + ), + {}, + ) + + @override + def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: + current_area = study_data.tree.get(["input", "areas", self.area_name, "ui"]) + + if self.layer == "0": + ui = current_area["ui"] + ui["x"] = self.new_area_parameters["x"] + ui["y"] = self.new_area_parameters["y"] + ui["color_r"], ui["color_g"], ui["color_b"] = self.new_area_parameters["color_rgb"] + current_area["layerX"][self.layer] = self.new_area_parameters["x"] + current_area["layerY"][self.layer] = self.new_area_parameters["y"] + current_area["layerColor"][self.layer] = ",".join(map(str, self.new_area_parameters["color_rgb"])) + + study_data.tree.save(current_area, ["input", "areas", self.area_name, "ui"]) + + output, _ = self._apply_config(study_data.config) + + return output + + @override + def to_dto(self) -> CommandDTO: + return CommandDTO( + action=CommandName.MOVE_AREA.value, + args={"area_name": self.area_name, "new_area_parameters": self.new_area_parameters, "layer": self.layer}, + study_version=self.study_version, + ) + + @override + def match_signature(self) -> str: + return str(self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.area_name) + + @override + def match(self, other: ICommand, equal: bool = False) -> bool: + if not isinstance(other, MoveArea): + return False + return self.area_name == other.area_name + + @override + def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: + return [] + + @override + def get_inner_matrices(self) -> t.List[str]: + return [] diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index 6f20f5873f..ea213915e6 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -183,41 +183,19 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): [ CommandDTO( id=None, - action=CommandName.UPDATE_CONFIG.value, - args=[ - { - "target": "input/areas/test/ui/ui/x", - "data": 100, - }, - { - "target": "input/areas/test/ui/ui/y", - "data": 200, - }, - { - "target": "input/areas/test/ui/ui/color_r", - "data": 255, - }, - { - "target": "input/areas/test/ui/ui/color_g", - "data": 0, - }, - { - "target": "input/areas/test/ui/ui/color_b", - "data": 100, - }, - { - "target": "input/areas/test/ui/layerX/0", - "data": 100, - }, - { - "target": "input/areas/test/ui/layerY/0", - "data": 200, - }, - { - "target": "input/areas/test/ui/layerColor/0", - "data": "255,0,100", + action=CommandName.MOVE_AREA.value, + args={ + "area_name": "test", + "new_area_parameters": { + "x": 100, + "y": 200, + "color_rgb": (255, 0, 100), + "layer_x": {}, + "layer_y": {}, + "layer_color": {}, }, - ], + "layer": "0", + }, study_version=study_version, ), ], diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index a2c392785a..3996c3bd94 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -39,6 +39,11 @@ ), CommandDTO(action=CommandName.REMOVE_AREA.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), CommandDTO(action=CommandName.REMOVE_AREA.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), + CommandDTO( + action=CommandName.MOVE_AREA.value, + args={"area_name": "id", "new_area_parameters": {}, "layer": "0"}, + study_version=STUDY_VERSION_8_8, + ), CommandDTO( action=CommandName.CREATE_DISTRICT.value, args={ From b8e41b7aa75aa6bbbf11e5f61aa56fd244e2105a Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Tue, 28 Jan 2025 13:58:13 +0100 Subject: [PATCH 3/9] fix(area): rename area service --- antarest/study/service.py | 18 ++++++++++-------- antarest/study/web/study_data_blueprint.py | 10 +++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/antarest/study/service.py b/antarest/study/service.py index 31bb80fa1f..0655e67a4e 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -403,7 +403,7 @@ def __init__( self.event_bus = event_bus self.file_transfer_manager = file_transfer_manager self.task_service = task_service - self.areas = AreaManager(self.storage_service, self.repository) + self.area_manager = AreaManager(self.storage_service, self.repository) self.district_manager = DistrictManager(self.storage_service) self.links_manager = LinkManager(self.storage_service) self.config_manager = ConfigManager(self.storage_service) @@ -426,7 +426,7 @@ def __init__( self.binding_constraint_manager = BindingConstraintManager(self.storage_service) self.correlation_manager = CorrelationManager(self.storage_service) self.table_mode_manager = TableModeManager( - self.areas, + self.area_manager, self.links_manager, self.thermal_manager, self.renewable_manager, @@ -1899,7 +1899,9 @@ def get_all_areas( ) -> t.Union[t.List[AreaInfoDTO], t.Dict[str, t.Any]]: study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.READ) - return self.areas.get_all_areas_ui_info(study) if ui else self.areas.get_all_areas(study, area_type) + return ( + self.area_manager.get_all_areas_ui_info(study) if ui else self.area_manager.get_all_areas(study, area_type) + ) def get_all_links( self, @@ -1919,7 +1921,7 @@ def create_area( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - new_area = self.areas.create_area(study, area_creation_dto) + new_area = self.area_manager.create_area(study, area_creation_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -1979,7 +1981,7 @@ def update_area( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - updated_area = self.areas.update_area_metadata(study, area_id, area_patch_dto) + updated_area = self.area_manager.update_area_metadata(study, area_id, area_patch_dto) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, @@ -2000,7 +2002,7 @@ def update_area_ui( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.areas.update_area_ui(study, area_id, area_ui, layer) + return self.area_manager.update_area_ui(study, area_id, area_ui, layer) def update_thermal_cluster_metadata( self, @@ -2012,7 +2014,7 @@ def update_thermal_cluster_metadata( study = self.get_study(uuid) assert_permission(params.user, study, StudyPermissionType.WRITE) self._assert_study_unarchived(study) - return self.areas.update_thermal_cluster_metadata(study, area_id, clusters_metadata) + return self.area_manager.update_thermal_cluster_metadata(study, area_id, clusters_metadata) def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> None: """ @@ -2036,7 +2038,7 @@ def delete_area(self, uuid: str, area_id: str, params: RequestParameters) -> Non if referencing_binding_constraints: binding_ids = [bc.id for bc in referencing_binding_constraints] raise ReferencedObjectDeletionNotAllowed(area_id, binding_ids, object_type="Area") - self.areas.delete_area(study, area_id) + self.area_manager.delete_area(study, area_id) self.event_bus.push( Event( type=EventType.STUDY_DATA_EDITED, diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 95659ecd7f..5b8064328a 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -323,7 +323,7 @@ def get_layers( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - return study_service.areas.get_layers(study) + return study_service.area_manager.get_layers(study) @bp.post( "/studies/{uuid}/layers", @@ -342,7 +342,7 @@ def create_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) - return study_service.areas.create_layer(study, name) + return study_service.area_manager.create_layer(study, name) @bp.put( "/studies/{uuid}/layers/{layer_id}", @@ -363,9 +363,9 @@ def update_layer( params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params) if name: - study_service.areas.update_layer_name(study, layer_id, name) + study_service.area_manager.update_layer_name(study, layer_id, name) if areas: - study_service.areas.update_layer_areas(study, layer_id, areas) + study_service.area_manager.update_layer_areas(study, layer_id, areas) @bp.delete( "/studies/{uuid}/layers/{layer_id}", @@ -385,7 +385,7 @@ def remove_layer( ) params = RequestParameters(user=current_user) study = study_service.check_study_access(uuid, StudyPermissionType.READ, params) - study_service.areas.remove_layer(study, layer_id) + study_service.area_manager.remove_layer(study, layer_id) @bp.get( "/studies/{uuid}/districts", From 41904286d0aa8744cca15d628b333b0524f24ee1 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Wed, 29 Jan 2025 11:58:33 +0100 Subject: [PATCH 4/9] feat(area): add a move_area command --- antarest/study/business/area_management.py | 75 ++------------- .../variantstudy/business/command_reverter.py | 5 + .../storage/variantstudy/command_factory.py | 2 + .../variantstudy/model/command/common.py | 1 + .../variantstudy/model/command/move_area.py | 96 +++++++++++++++++++ .../storage/business/test_arealink_manager.py | 46 +++------ tests/variantstudy/test_command_factory.py | 5 + 7 files changed, 131 insertions(+), 99 deletions(-) create mode 100644 antarest/study/storage/variantstudy/model/command/move_area.py diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index c607be980d..1871c876ac 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -37,6 +37,7 @@ from antarest.study.storage.storage_service import StudyStorageService from antarest.study.storage.variantstudy.model.command.create_area import CreateArea from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.move_area import MoveArea from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig @@ -680,73 +681,17 @@ def update_area_metadata( ) def update_area_ui(self, study: Study, area_id: str, area_ui: UpdateAreaUi, layer: str = "0") -> None: - obj = { - "x": area_ui.x, - "y": area_ui.y, - "color_r": area_ui.color_rgb[0], - "color_g": area_ui.color_rgb[1], - "color_b": area_ui.color_rgb[2], - } file_study = self.storage_service.get_storage(study).get_raw(study) - commands = ( - [ - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/x", - data=obj["x"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/y", - data=obj["y"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/color_r", - data=obj["color_r"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/color_g", - data=obj["color_g"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/ui/color_b", - data=obj["color_b"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - ] - if layer == "0" - else [] - ) - commands.extend( - [ - UpdateConfig( - target=f"input/areas/{area_id}/ui/layerX/{layer}", - data=obj["x"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/layerY/{layer}", - data=obj["y"], - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - UpdateConfig( - target=f"input/areas/{area_id}/ui/layerColor/{layer}", - data=f"{obj['color_r']},{obj['color_g']},{obj['color_b']}", - command_context=self.storage_service.variant_study_service.command_factory.command_context, - study_version=file_study.config.version, - ), - ] + + command = MoveArea( + area_name=area_id, + new_area_parameters=area_ui.model_dump(), + layer=layer, + 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, commands, self.storage_service) + + execute_or_add_commands(study, file_study, [command], self.storage_service) def update_thermal_cluster_metadata( self, diff --git a/antarest/study/storage/variantstudy/business/command_reverter.py b/antarest/study/storage/variantstudy/business/command_reverter.py index 4b74d2a0ba..20243375e0 100644 --- a/antarest/study/storage/variantstudy/business/command_reverter.py +++ b/antarest/study/storage/variantstudy/business/command_reverter.py @@ -33,6 +33,7 @@ GenerateThermalClusterTimeSeries, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.move_area import MoveArea from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.remove_binding_constraint import RemoveBindingConstraint from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster @@ -72,6 +73,10 @@ def _revert_create_area(base_command: CreateArea, history: t.List["ICommand"], b def _revert_remove_area(base_command: RemoveArea, history: t.List["ICommand"], base: FileStudy) -> t.List[ICommand]: raise NotImplementedError("The revert function for RemoveArea is not available") + @staticmethod + def _revert_move_area(base_command: MoveArea, history: t.List["ICommand"], base: FileStudy) -> t.List[ICommand]: + raise NotImplementedError("The revert function for MoveArea is not available") + @staticmethod def _revert_create_district( base_command: CreateDistrict, diff --git a/antarest/study/storage/variantstudy/command_factory.py b/antarest/study/storage/variantstudy/command_factory.py index f20f0ef58d..8696f0f547 100644 --- a/antarest/study/storage/variantstudy/command_factory.py +++ b/antarest/study/storage/variantstudy/command_factory.py @@ -32,6 +32,7 @@ GenerateThermalClusterTimeSeries, ) from antarest.study.storage.variantstudy.model.command.icommand import ICommand +from antarest.study.storage.variantstudy.model.command.move_area import MoveArea from antarest.study.storage.variantstudy.model.command.remove_area import RemoveArea from antarest.study.storage.variantstudy.model.command.remove_binding_constraint import RemoveBindingConstraint from antarest.study.storage.variantstudy.model.command.remove_cluster import RemoveCluster @@ -57,6 +58,7 @@ COMMAND_MAPPING = { CommandName.CREATE_AREA.value: CreateArea, + CommandName.MOVE_AREA.value: MoveArea, CommandName.REMOVE_AREA.value: RemoveArea, CommandName.CREATE_DISTRICT.value: CreateDistrict, CommandName.REMOVE_DISTRICT.value: RemoveDistrict, diff --git a/antarest/study/storage/variantstudy/model/command/common.py b/antarest/study/storage/variantstudy/model/command/common.py index e91a0e2e58..c75bee0d47 100644 --- a/antarest/study/storage/variantstudy/model/command/common.py +++ b/antarest/study/storage/variantstudy/model/command/common.py @@ -30,6 +30,7 @@ class FilteringOptions: class CommandName(Enum): CREATE_AREA = "create_area" + MOVE_AREA = "move_area" REMOVE_AREA = "remove_area" CREATE_DISTRICT = "create_district" REMOVE_DISTRICT = "remove_district" diff --git a/antarest/study/storage/variantstudy/model/command/move_area.py b/antarest/study/storage/variantstudy/model/command/move_area.py new file mode 100644 index 0000000000..08ab6f5b1f --- /dev/null +++ b/antarest/study/storage/variantstudy/model/command/move_area.py @@ -0,0 +1,96 @@ +# 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 typing as t + +from typing_extensions import override + +from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy +from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput +from antarest.study.storage.variantstudy.model.command.icommand import MATCH_SIGNATURE_SEPARATOR, ICommand +from antarest.study.storage.variantstudy.model.command_listener.command_listener import ICommandListener +from antarest.study.storage.variantstudy.model.model import CommandDTO + + +class MoveArea(ICommand): + """ + Command used to move an area in the study. + """ + + # Overloaded metadata + # =================== + + command_name: CommandName = CommandName.MOVE_AREA + version: int = 1 + + # Command parameters + # ================== + + area_name: str + new_area_parameters: t.Dict[str, t.Any] + layer: str + + @override + def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: + return ( + CommandOutput( + status=True, + message=f"area '{self.area_name}' updated", + ), + {}, + ) + + @override + def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: + current_area = study_data.tree.get(["input", "areas", self.area_name, "ui"]) + + if self.layer == "0": + ui = current_area["ui"] + ui["x"] = self.new_area_parameters["x"] + ui["y"] = self.new_area_parameters["y"] + ui["color_r"], ui["color_g"], ui["color_b"] = self.new_area_parameters["color_rgb"] + current_area["layerX"][self.layer] = self.new_area_parameters["x"] + current_area["layerY"][self.layer] = self.new_area_parameters["y"] + current_area["layerColor"][self.layer] = ",".join(map(str, self.new_area_parameters["color_rgb"])) + + study_data.tree.save(current_area, ["input", "areas", self.area_name, "ui"]) + + output, _ = self._apply_config(study_data.config) + + return output + + @override + def to_dto(self) -> CommandDTO: + return CommandDTO( + action=CommandName.MOVE_AREA.value, + args={"area_name": self.area_name, "new_area_parameters": self.new_area_parameters, "layer": self.layer}, + study_version=self.study_version, + ) + + @override + def match_signature(self) -> str: + return str(self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.area_name) + + @override + def match(self, other: ICommand, equal: bool = False) -> bool: + if not isinstance(other, MoveArea): + return False + return self.area_name == other.area_name + + @override + def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: + return [] + + @override + def get_inner_matrices(self) -> t.List[str]: + return [] diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index 6f20f5873f..ea213915e6 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -183,41 +183,19 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): [ CommandDTO( id=None, - action=CommandName.UPDATE_CONFIG.value, - args=[ - { - "target": "input/areas/test/ui/ui/x", - "data": 100, - }, - { - "target": "input/areas/test/ui/ui/y", - "data": 200, - }, - { - "target": "input/areas/test/ui/ui/color_r", - "data": 255, - }, - { - "target": "input/areas/test/ui/ui/color_g", - "data": 0, - }, - { - "target": "input/areas/test/ui/ui/color_b", - "data": 100, - }, - { - "target": "input/areas/test/ui/layerX/0", - "data": 100, - }, - { - "target": "input/areas/test/ui/layerY/0", - "data": 200, - }, - { - "target": "input/areas/test/ui/layerColor/0", - "data": "255,0,100", + action=CommandName.MOVE_AREA.value, + args={ + "area_name": "test", + "new_area_parameters": { + "x": 100, + "y": 200, + "color_rgb": (255, 0, 100), + "layer_x": {}, + "layer_y": {}, + "layer_color": {}, }, - ], + "layer": "0", + }, study_version=study_version, ), ], diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index a2c392785a..3996c3bd94 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -39,6 +39,11 @@ ), CommandDTO(action=CommandName.REMOVE_AREA.value, args={"id": "id"}, study_version=STUDY_VERSION_8_8), CommandDTO(action=CommandName.REMOVE_AREA.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), + CommandDTO( + action=CommandName.MOVE_AREA.value, + args={"area_name": "id", "new_area_parameters": {}, "layer": "0"}, + study_version=STUDY_VERSION_8_8, + ), CommandDTO( action=CommandName.CREATE_DISTRICT.value, args={ From ea1f96c2ebd641002b264869d7d671c786fb5773 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Thu, 30 Jan 2025 08:58:08 +0100 Subject: [PATCH 5/9] feat(area): rename area_name to area_id --- antarest/study/business/area_management.py | 2 +- .../variantstudy/model/command/move_area.py | 14 +++++++------- tests/storage/business/test_arealink_manager.py | 2 +- tests/variantstudy/test_command_factory.py | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 1871c876ac..854101bc25 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -684,7 +684,7 @@ def update_area_ui(self, study: Study, area_id: str, area_ui: UpdateAreaUi, laye file_study = self.storage_service.get_storage(study).get_raw(study) command = MoveArea( - area_name=area_id, + area_id=area_id, new_area_parameters=area_ui.model_dump(), layer=layer, command_context=self.storage_service.variant_study_service.command_factory.command_context, diff --git a/antarest/study/storage/variantstudy/model/command/move_area.py b/antarest/study/storage/variantstudy/model/command/move_area.py index 08ab6f5b1f..e98f03f493 100644 --- a/antarest/study/storage/variantstudy/model/command/move_area.py +++ b/antarest/study/storage/variantstudy/model/command/move_area.py @@ -36,7 +36,7 @@ class MoveArea(ICommand): # Command parameters # ================== - area_name: str + area_id: str new_area_parameters: t.Dict[str, t.Any] layer: str @@ -45,14 +45,14 @@ def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutpu return ( CommandOutput( status=True, - message=f"area '{self.area_name}' updated", + message=f"area '{self.area_id}' updated", ), {}, ) @override def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: - current_area = study_data.tree.get(["input", "areas", self.area_name, "ui"]) + current_area = study_data.tree.get(["input", "areas", self.area_id, "ui"]) if self.layer == "0": ui = current_area["ui"] @@ -63,7 +63,7 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = current_area["layerY"][self.layer] = self.new_area_parameters["y"] current_area["layerColor"][self.layer] = ",".join(map(str, self.new_area_parameters["color_rgb"])) - study_data.tree.save(current_area, ["input", "areas", self.area_name, "ui"]) + study_data.tree.save(current_area, ["input", "areas", self.area_id, "ui"]) output, _ = self._apply_config(study_data.config) @@ -73,19 +73,19 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = def to_dto(self) -> CommandDTO: return CommandDTO( action=CommandName.MOVE_AREA.value, - args={"area_name": self.area_name, "new_area_parameters": self.new_area_parameters, "layer": self.layer}, + args={"area_id": self.area_id, "new_area_parameters": self.new_area_parameters, "layer": self.layer}, study_version=self.study_version, ) @override def match_signature(self) -> str: - return str(self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.area_name) + return str(self.command_name.value + MATCH_SIGNATURE_SEPARATOR + self.area_id) @override def match(self, other: ICommand, equal: bool = False) -> bool: if not isinstance(other, MoveArea): return False - return self.area_name == other.area_name + return self.area_id == other.area_id @override def _create_diff(self, other: "ICommand") -> t.List["ICommand"]: diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index ea213915e6..5434c622cc 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -185,7 +185,7 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): id=None, action=CommandName.MOVE_AREA.value, args={ - "area_name": "test", + "area_id": "test", "new_area_parameters": { "x": 100, "y": 200, diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 3996c3bd94..31f05f8140 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -41,7 +41,7 @@ CommandDTO(action=CommandName.REMOVE_AREA.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), CommandDTO( action=CommandName.MOVE_AREA.value, - args={"area_name": "id", "new_area_parameters": {}, "layer": "0"}, + args={"area_id": "id", "new_area_parameters": {}, "layer": "0"}, study_version=STUDY_VERSION_8_8, ), CommandDTO( From dc800d7c8e6d9eeef6bda6b82fbc8471d6dfeae2 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Thu, 30 Jan 2025 09:43:54 +0100 Subject: [PATCH 6/9] feat(area): minor fixes --- .../storage/variantstudy/model/command/move_area.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/antarest/study/storage/variantstudy/model/command/move_area.py b/antarest/study/storage/variantstudy/model/command/move_area.py index e98f03f493..8de6b1cce3 100644 --- a/antarest/study/storage/variantstudy/model/command/move_area.py +++ b/antarest/study/storage/variantstudy/model/command/move_area.py @@ -24,7 +24,7 @@ class MoveArea(ICommand): """ - Command used to move an area in the study. + Command used to move an area inside the map and to update its UI. """ # Overloaded metadata @@ -42,13 +42,7 @@ class MoveArea(ICommand): @override def _apply_config(self, study_data: FileStudyTreeConfig) -> t.Tuple[CommandOutput, t.Dict[str, t.Any]]: - return ( - CommandOutput( - status=True, - message=f"area '{self.area_id}' updated", - ), - {}, - ) + return CommandOutput(status=True, message=f"area '{self.area_id}' UI updated"), {} @override def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = None) -> CommandOutput: From c9fcd014cdf32f261e2deaa829aff1a2b7f624c9 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Thu, 30 Jan 2025 10:43:28 +0100 Subject: [PATCH 7/9] feat(area): extract area objects to area model --- .../study/business/allocation_management.py | 2 +- antarest/study/business/area_management.py | 246 +---------------- .../study/business/correlation_management.py | 2 +- antarest/study/business/model/area_model.py | 247 ++++++++++++++++++ .../study/business/table_mode_management.py | 3 +- antarest/study/service.py | 3 +- .../variantstudy/model/command/move_area.py | 17 +- antarest/study/web/study_data_blueprint.py | 2 +- .../storage/business/test_arealink_manager.py | 11 +- tests/variantstudy/test_command_factory.py | 7 +- 10 files changed, 285 insertions(+), 255 deletions(-) create mode 100644 antarest/study/business/model/area_model.py diff --git a/antarest/study/business/allocation_management.py b/antarest/study/business/allocation_management.py index eafec7f479..e1901b0b75 100644 --- a/antarest/study/business/allocation_management.py +++ b/antarest/study/business/allocation_management.py @@ -19,7 +19,7 @@ from typing_extensions import Annotated from antarest.core.exceptions import AllocationDataNotFound, AreaNotFound -from antarest.study.business.area_management import AreaInfoDTO +from antarest.study.business.model.area_model import AreaInfoDTO from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands from antarest.study.model import Study from antarest.study.storage.storage_service import StudyStorageService diff --git a/antarest/study/business/area_management.py b/antarest/study/business/area_management.py index 854101bc25..7fe58c346b 100644 --- a/antarest/study/business/area_management.py +++ b/antarest/study/business/area_management.py @@ -10,28 +10,27 @@ # # This file is part of the Antares project. -import enum import logging -import re import typing as t -from pydantic import Field - from antarest.core.exceptions import ConfigFileNotFound, DuplicateAreaName, LayerNotAllowedToBeDeleted, LayerNotFound from antarest.core.model import JSON -from antarest.core.serialization import AntaresBaseModel -from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model +from antarest.study.business.model.area_model import ( + AreaCreationDTO, + AreaInfoDTO, + AreaOutput, + AreaType, + 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 ( - AdequacyPathProperties, - AreaFolder, - OptimizationProperties, - ThermalAreasProperties, - UIProperties, -) +from antarest.study.storage.rawstudy.model.filesystem.config.area import AreaFolder, ThermalAreasProperties 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 @@ -44,229 +43,10 @@ logger = logging.getLogger(__name__) -class AreaType(enum.Enum): - AREA = "AREA" - DISTRICT = "DISTRICT" - - -class AreaCreationDTO(AntaresBaseModel): - name: str - type: AreaType - metadata: t.Optional[PatchArea] = None - set: t.Optional[t.List[str]] = None - - -# review: is this class necessary? -class ClusterInfoDTO(PatchCluster): - id: str - name: str - enabled: bool = True - unitcount: int = 0 - nominalcapacity: float = 0 - group: t.Optional[str] = None - min_stable_power: t.Optional[float] = None - min_up_time: t.Optional[int] = None - min_down_time: t.Optional[int] = None - spinning: t.Optional[float] = None - marginal_cost: t.Optional[float] = None - spread_cost: t.Optional[float] = None - market_bid_cost: t.Optional[float] = None - - -class AreaInfoDTO(AreaCreationDTO): - id: str - thermals: t.Optional[t.List[ClusterInfoDTO]] = None - - -class LayerInfoDTO(AntaresBaseModel): - id: str - name: str - areas: t.List[str] - - -class UpdateAreaUi(AntaresBaseModel, extra="forbid", populate_by_name=True): - """ - DTO for updating area UI - - Usage: - - >>> from antarest.study.business.area_management 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") - layer_x: t.Mapping[int, int] = Field(default_factory=dict, title="X position of each layer", alias="layerX") - layer_y: t.Mapping[int, int] = Field(default_factory=dict, title="Y position of each layer", alias="layerY") - 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 [] - - _ALL_AREAS_PATH = "input/areas" _THERMAL_AREAS_PATH = "input/thermal/areas" -# noinspection SpellCheckingInspection -class _BaseAreaDTO( - OptimizationProperties.FilteringSection, - OptimizationProperties.ModalOptimizationSection, - AdequacyPathProperties.AdequacyPathSection, - extra="forbid", - validate_assignment=True, - populate_by_name=True, -): - """ - Represents an area output. - - Aggregates the fields of the `OptimizationProperties` and `AdequacyPathProperties` classes, - but without the `UIProperties` fields. - - Add the fields extracted from the `/input/thermal/areas.ini` information: - - - `average_unsupplied_energy_cost` is extracted from `unserverd_energy_cost`, - - `average_spilled_energy_cost` is extracted from `spilled_energy_cost`. - """ - - average_unsupplied_energy_cost: float = Field(0.0, description="average unserverd energy cost (€/MWh)") - average_spilled_energy_cost: float = Field(0.0, description="average spilled energy cost (€/MWh)") - - -# noinspection SpellCheckingInspection -@all_optional_model -@camel_case_model -class AreaOutput(_BaseAreaDTO): - """ - DTO object use to get the area information using a flat structure. - """ - - @classmethod - def from_model( - cls, - area_folder: AreaFolder, - *, - average_unsupplied_energy_cost: float, - average_spilled_energy_cost: float, - ) -> "AreaOutput": - """ - Creates a `GetAreaDTO` object from configuration data. - - Args: - area_folder: Configuration data read from the `/input/areas/` information. - average_unsupplied_energy_cost: Unserverd energy cost (€/MWh). - average_spilled_energy_cost: Spilled energy cost (€/MWh). - Returns: - The `GetAreaDTO` object. - """ - obj = { - "average_unsupplied_energy_cost": average_unsupplied_energy_cost, - "average_spilled_energy_cost": average_spilled_energy_cost, - **area_folder.optimization.filtering.model_dump(mode="json", by_alias=False), - **area_folder.optimization.nodal_optimization.model_dump(mode="json", by_alias=False), - # adequacy_patch is only available if study version >= 830. - **( - area_folder.adequacy_patch.adequacy_patch.model_dump(mode="json", by_alias=False) - if area_folder.adequacy_patch - else {} - ), - } - return cls(**obj) - - def _to_optimization(self) -> OptimizationProperties: - obj = {name: getattr(self, name) for name in OptimizationProperties.FilteringSection.model_fields} - filtering_section = OptimizationProperties.FilteringSection(**obj) - obj = {name: getattr(self, name) for name in OptimizationProperties.ModalOptimizationSection.model_fields} - nodal_optimization_section = OptimizationProperties.ModalOptimizationSection(**obj) - args = {"filtering": filtering_section, "nodal_optimization": nodal_optimization_section} - return OptimizationProperties.model_validate(args) - - def _to_adequacy_patch(self) -> t.Optional[AdequacyPathProperties]: - obj = {name: getattr(self, name) for name in AdequacyPathProperties.AdequacyPathSection.model_fields} - # If all fields are `None`, the object is empty. - if all(value is None for value in obj.values()): - return None - adequacy_path_section = AdequacyPathProperties.AdequacyPathSection(**obj) - return AdequacyPathProperties.model_validate({"adequacy_patch": adequacy_path_section}) - - @property - def area_folder(self) -> AreaFolder: - area_folder = AreaFolder( - optimization=self._to_optimization(), - adequacy_patch=self._to_adequacy_patch(), - # UI properties are not configurable in Table Mode - ) - return area_folder - - class AreaManager: """ Manages operations related to areas in a study, including retrieval, creation, and updates. @@ -685,7 +465,7 @@ def update_area_ui(self, study: Study, area_id: str, area_ui: UpdateAreaUi, laye command = MoveArea( area_id=area_id, - new_area_parameters=area_ui.model_dump(), + area_ui=area_ui, layer=layer, command_context=self.storage_service.variant_study_service.command_factory.command_context, study_version=file_study.config.version, diff --git a/antarest/study/business/correlation_management.py b/antarest/study/business/correlation_management.py index 6aab755ab8..76a940fa70 100644 --- a/antarest/study/business/correlation_management.py +++ b/antarest/study/business/correlation_management.py @@ -22,7 +22,7 @@ from pydantic import ValidationInfo, field_validator from antarest.core.exceptions import AreaNotFound -from antarest.study.business.area_management import AreaInfoDTO +from antarest.study.business.model.area_model import AreaInfoDTO from antarest.study.business.utils import FormFieldsBaseModel, execute_or_add_commands from antarest.study.model import Study from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy diff --git a/antarest/study/business/model/area_model.py b/antarest/study/business/model/area_model.py new file mode 100644 index 0000000000..b304286d16 --- /dev/null +++ b/antarest/study/business/model/area_model.py @@ -0,0 +1,247 @@ +# 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 enum +import re +import typing as t + +from pydantic import Field + +from antarest.core.serialization import AntaresBaseModel +from antarest.study.business.all_optional_meta import all_optional_model, camel_case_model +from antarest.study.model import PatchArea, PatchCluster +from antarest.study.storage.rawstudy.model.filesystem.config.area import ( + AdequacyPathProperties, + AreaFolder, + OptimizationProperties, + UIProperties, +) +from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy + + +class AreaType(enum.Enum): + AREA = "AREA" + DISTRICT = "DISTRICT" + + +class AreaCreationDTO(AntaresBaseModel): + name: str + type: AreaType + metadata: t.Optional[PatchArea] = None + set: t.Optional[t.List[str]] = None + + +# review: is this class necessary? +class ClusterInfoDTO(PatchCluster): + id: str + name: str + enabled: bool = True + unitcount: int = 0 + nominalcapacity: float = 0 + group: t.Optional[str] = None + min_stable_power: t.Optional[float] = None + min_up_time: t.Optional[int] = None + min_down_time: t.Optional[int] = None + spinning: t.Optional[float] = None + marginal_cost: t.Optional[float] = None + spread_cost: t.Optional[float] = None + market_bid_cost: t.Optional[float] = None + + +class AreaInfoDTO(AreaCreationDTO): + id: str + thermals: t.Optional[t.List[ClusterInfoDTO]] = None + + +class LayerInfoDTO(AntaresBaseModel): + id: str + name: str + areas: t.List[str] + + +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") + layer_x: t.Mapping[int, int] = Field(default_factory=dict, title="X position of each layer", alias="layerX") + layer_y: t.Mapping[int, int] = Field(default_factory=dict, title="Y position of each layer", alias="layerY") + 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, + OptimizationProperties.ModalOptimizationSection, + AdequacyPathProperties.AdequacyPathSection, + extra="forbid", + validate_assignment=True, + populate_by_name=True, +): + """ + Represents an area output. + + Aggregates the fields of the `OptimizationProperties` and `AdequacyPathProperties` classes, + but without the `UIProperties` fields. + + Add the fields extracted from the `/input/thermal/areas.ini` information: + + - `average_unsupplied_energy_cost` is extracted from `unserverd_energy_cost`, + - `average_spilled_energy_cost` is extracted from `spilled_energy_cost`. + """ + + average_unsupplied_energy_cost: float = Field(0.0, description="average unserverd energy cost (€/MWh)") + average_spilled_energy_cost: float = Field(0.0, description="average spilled energy cost (€/MWh)") + + +# noinspection SpellCheckingInspection +@all_optional_model +@camel_case_model +class AreaOutput(_BaseAreaDTO): + """ + DTO object use to get the area information using a flat structure. + """ + + @classmethod + def from_model( + cls, + area_folder: AreaFolder, + *, + average_unsupplied_energy_cost: float, + average_spilled_energy_cost: float, + ) -> "AreaOutput": + """ + Creates a `GetAreaDTO` object from configuration data. + + Args: + area_folder: Configuration data read from the `/input/areas/` information. + average_unsupplied_energy_cost: Unserverd energy cost (€/MWh). + average_spilled_energy_cost: Spilled energy cost (€/MWh). + Returns: + The `GetAreaDTO` object. + """ + obj = { + "average_unsupplied_energy_cost": average_unsupplied_energy_cost, + "average_spilled_energy_cost": average_spilled_energy_cost, + **area_folder.optimization.filtering.model_dump(mode="json", by_alias=False), + **area_folder.optimization.nodal_optimization.model_dump(mode="json", by_alias=False), + # adequacy_patch is only available if study version >= 830. + **( + area_folder.adequacy_patch.adequacy_patch.model_dump(mode="json", by_alias=False) + if area_folder.adequacy_patch + else {} + ), + } + return cls(**obj) + + def _to_optimization(self) -> OptimizationProperties: + obj = {name: getattr(self, name) for name in OptimizationProperties.FilteringSection.model_fields} + filtering_section = OptimizationProperties.FilteringSection(**obj) + obj = {name: getattr(self, name) for name in OptimizationProperties.ModalOptimizationSection.model_fields} + nodal_optimization_section = OptimizationProperties.ModalOptimizationSection(**obj) + args = {"filtering": filtering_section, "nodal_optimization": nodal_optimization_section} + return OptimizationProperties.model_validate(args) + + def _to_adequacy_patch(self) -> t.Optional[AdequacyPathProperties]: + obj = {name: getattr(self, name) for name in AdequacyPathProperties.AdequacyPathSection.model_fields} + # If all fields are `None`, the object is empty. + if all(value is None for value in obj.values()): + return None + adequacy_path_section = AdequacyPathProperties.AdequacyPathSection(**obj) + return AdequacyPathProperties.model_validate({"adequacy_patch": adequacy_path_section}) + + @property + def area_folder(self) -> AreaFolder: + area_folder = AreaFolder( + optimization=self._to_optimization(), + adequacy_patch=self._to_adequacy_patch(), + # UI properties are not configurable in Table Mode + ) + return area_folder diff --git a/antarest/study/business/table_mode_management.py b/antarest/study/business/table_mode_management.py index efaeeae5c7..2ede027bc3 100644 --- a/antarest/study/business/table_mode_management.py +++ b/antarest/study/business/table_mode_management.py @@ -20,13 +20,14 @@ from antarest.core.exceptions import ChildNotFoundError from antarest.core.model import JSON -from antarest.study.business.area_management import AreaManager, AreaOutput +from antarest.study.business.area_management import AreaManager from antarest.study.business.areas.renewable_management import RenewableClusterInput, RenewableManager from antarest.study.business.areas.st_storage_management import STStorageInput, STStorageManager from antarest.study.business.areas.thermal_management import ThermalClusterInput, ThermalManager from antarest.study.business.binding_constraint_management import BindingConstraintManager, ConstraintInput from antarest.study.business.enum_ignore_case import EnumIgnoreCase from antarest.study.business.link_management import LinkManager +from antarest.study.business.model.area_model import AreaOutput from antarest.study.business.model.link_model import LinkBaseDTO from antarest.study.model import STUDY_VERSION_8_2, RawStudy diff --git a/antarest/study/service.py b/antarest/study/service.py index 0655e67a4e..c9ee791f54 100644 --- a/antarest/study/service.py +++ b/antarest/study/service.py @@ -79,7 +79,7 @@ MCIndLinksQueryFile, ) from antarest.study.business.allocation_management import AllocationManager -from antarest.study.business.area_management import AreaCreationDTO, AreaInfoDTO, AreaManager, AreaType, UpdateAreaUi +from antarest.study.business.area_management import AreaManager from antarest.study.business.areas.hydro_management import HydroManager from antarest.study.business.areas.properties_management import PropertiesManager from antarest.study.business.areas.renewable_management import RenewableManager @@ -92,6 +92,7 @@ from antarest.study.business.general_management import GeneralManager from antarest.study.business.link_management import LinkManager from antarest.study.business.matrix_management import MatrixManager, MatrixManagerError +from antarest.study.business.model.area_model import AreaCreationDTO, AreaInfoDTO, AreaType, UpdateAreaUi from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO from antarest.study.business.optimization_management import OptimizationManager from antarest.study.business.playlist_management import PlaylistManager diff --git a/antarest/study/storage/variantstudy/model/command/move_area.py b/antarest/study/storage/variantstudy/model/command/move_area.py index 8de6b1cce3..41c1aced21 100644 --- a/antarest/study/storage/variantstudy/model/command/move_area.py +++ b/antarest/study/storage/variantstudy/model/command/move_area.py @@ -14,6 +14,7 @@ from typing_extensions import override +from antarest.study.business.model.area_model import UpdateAreaUi from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy from antarest.study.storage.variantstudy.model.command.common import CommandName, CommandOutput @@ -37,7 +38,7 @@ class MoveArea(ICommand): # ================== area_id: str - new_area_parameters: t.Dict[str, t.Any] + area_ui: UpdateAreaUi layer: str @override @@ -50,12 +51,12 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = if self.layer == "0": ui = current_area["ui"] - ui["x"] = self.new_area_parameters["x"] - ui["y"] = self.new_area_parameters["y"] - ui["color_r"], ui["color_g"], ui["color_b"] = self.new_area_parameters["color_rgb"] - current_area["layerX"][self.layer] = self.new_area_parameters["x"] - current_area["layerY"][self.layer] = self.new_area_parameters["y"] - current_area["layerColor"][self.layer] = ",".join(map(str, self.new_area_parameters["color_rgb"])) + ui["x"] = self.area_ui.x + ui["y"] = self.area_ui.y + ui["color_r"], ui["color_g"], ui["color_b"] = self.area_ui.color_rgb + current_area["layerX"][self.layer] = self.area_ui.x + current_area["layerY"][self.layer] = self.area_ui.y + current_area["layerColor"][self.layer] = ",".join(map(str, self.area_ui.color_rgb)) study_data.tree.save(current_area, ["input", "areas", self.area_id, "ui"]) @@ -67,7 +68,7 @@ def _apply(self, study_data: FileStudy, listener: t.Optional[ICommandListener] = def to_dto(self) -> CommandDTO: return CommandDTO( action=CommandName.MOVE_AREA.value, - args={"area_id": self.area_id, "new_area_parameters": self.new_area_parameters, "layer": self.layer}, + args={"area_id": self.area_id, "area_ui": self.area_ui, "layer": self.layer}, study_version=self.study_version, ) diff --git a/antarest/study/web/study_data_blueprint.py b/antarest/study/web/study_data_blueprint.py index 5b8064328a..9a33246ec4 100644 --- a/antarest/study/web/study_data_blueprint.py +++ b/antarest/study/web/study_data_blueprint.py @@ -30,7 +30,6 @@ from antarest.study.business.adequacy_patch_management import AdequacyPatchFormFields from antarest.study.business.advanced_parameters_management import AdvancedParamsFormFields from antarest.study.business.allocation_management import AllocationField, AllocationFormFields, AllocationMatrix -from antarest.study.business.area_management import AreaCreationDTO, AreaInfoDTO, AreaType, LayerInfoDTO, UpdateAreaUi from antarest.study.business.areas.hydro_management import InflowStructure, ManagementOptionsFormFields from antarest.study.business.areas.properties_management import PropertiesFormFields from antarest.study.business.areas.renewable_management import ( @@ -68,6 +67,7 @@ ) from antarest.study.business.district_manager import DistrictCreationDTO, DistrictInfoDTO, DistrictUpdateDTO from antarest.study.business.general_management import GeneralFormFields +from antarest.study.business.model.area_model import AreaCreationDTO, AreaInfoDTO, AreaType, LayerInfoDTO, UpdateAreaUi from antarest.study.business.model.link_model import LinkBaseDTO, LinkDTO from antarest.study.business.optimization_management import OptimizationFormFields from antarest.study.business.playlist_management import PlaylistColumns diff --git a/tests/storage/business/test_arealink_manager.py b/tests/storage/business/test_arealink_manager.py index 5434c622cc..bef6dc151c 100644 --- a/tests/storage/business/test_arealink_manager.py +++ b/tests/storage/business/test_arealink_manager.py @@ -186,14 +186,9 @@ def test_area_crud(empty_study: FileStudy, matrix_service: SimpleMatrixService): action=CommandName.MOVE_AREA.value, args={ "area_id": "test", - "new_area_parameters": { - "x": 100, - "y": 200, - "color_rgb": (255, 0, 100), - "layer_x": {}, - "layer_y": {}, - "layer_color": {}, - }, + "area_ui": UpdateAreaUi( + x=100, y=200, color_rgb=(255, 0, 100), layer_x={}, layer_y={}, layer_color={} + ), "layer": "0", }, study_version=study_version, diff --git a/tests/variantstudy/test_command_factory.py b/tests/variantstudy/test_command_factory.py index 31f05f8140..91ff146298 100644 --- a/tests/variantstudy/test_command_factory.py +++ b/tests/variantstudy/test_command_factory.py @@ -19,6 +19,7 @@ import pytest from antarest.matrixstore.service import MatrixService +from antarest.study.business.model.area_model import UpdateAreaUi from antarest.study.model import STUDY_VERSION_8_8 from antarest.study.storage.patch_service import PatchService from antarest.study.storage.variantstudy.business.matrix_constants_generator import GeneratorMatrixConstants @@ -41,7 +42,11 @@ CommandDTO(action=CommandName.REMOVE_AREA.value, args=[{"id": "id"}], study_version=STUDY_VERSION_8_8), CommandDTO( action=CommandName.MOVE_AREA.value, - args={"area_id": "id", "new_area_parameters": {}, "layer": "0"}, + args={ + "area_id": "id", + "area_ui": UpdateAreaUi(x=100, y=100, color_rgb=(100, 100, 100), layer_x={}, layer_y={}, layer_color={}), + "layer": "0", + }, study_version=STUDY_VERSION_8_8, ), CommandDTO( From f8f157055d18a4d9280eff8f914095fcb6ac7c86 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Thu, 30 Jan 2025 13:23:44 +0100 Subject: [PATCH 8/9] feat(load): add area integration test --- antarest/study/business/area_management.py | 52 +++++++++++- antarest/study/business/model/area_model.py | 81 +------------------ .../study_data_blueprint/test_area.py | 79 ++++++++++++++++++ 3 files changed, 128 insertions(+), 84 deletions(-) create mode 100644 tests/integration/study_data_blueprint/test_area.py 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 From e6b6743b20cccda8eb8ad091a378a6cec1e32647 Mon Sep 17 00:00:00 2001 From: Theo Pascoli Date: Thu, 30 Jan 2025 13:54:25 +0100 Subject: [PATCH 9/9] feat(load): minor fix --- antarest/study/business/model/area_model.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/antarest/study/business/model/area_model.py b/antarest/study/business/model/area_model.py index c1357e66fa..c67e0fe739 100644 --- a/antarest/study/business/model/area_model.py +++ b/antarest/study/business/model/area_model.py @@ -9,9 +9,8 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -True + import enum -import re import typing as t from pydantic import Field @@ -23,9 +22,7 @@ AdequacyPathProperties, AreaFolder, OptimizationProperties, - UIProperties, ) -from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy class AreaType(enum.Enum):