diff --git a/antarest/study/storage/variantstudy/business/utils.py b/antarest/study/storage/variantstudy/business/utils.py index 6f04601ec5..933c72bed7 100644 --- a/antarest/study/storage/variantstudy/business/utils.py +++ b/antarest/study/storage/variantstudy/business/utils.py @@ -52,10 +52,13 @@ def get_or_create_section(json_ini: JSON, section: str) -> JSON: def remove_none_args(command_dto: CommandDTO) -> CommandDTO: - if isinstance(command_dto.args, list): - command_dto.args = [{k: v for k, v in args.items() if v is not None} for args in command_dto.args] + args = command_dto.args + if isinstance(args, list): + command_dto.args = [{k: v for k, v in args.items() if v is not None} for args in args] + elif isinstance(args, dict): + command_dto.args = {k: v for k, v in args.items() if v is not None} else: - command_dto.args = {k: v for k, v in command_dto.args.items() if v is not None} + raise TypeError(f"Invalid type for args: {type(args)}") return command_dto diff --git a/antarest/study/storage/variantstudy/model/model.py b/antarest/study/storage/variantstudy/model/model.py index 1e51032ce4..cd478742b4 100644 --- a/antarest/study/storage/variantstudy/model/model.py +++ b/antarest/study/storage/variantstudy/model/model.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Tuple, Union +import typing as t from pydantic import BaseModel @@ -7,28 +7,63 @@ class GenerationResultInfoDTO(BaseModel): + """ + Result information of a snapshot generation process. + + Attributes: + success: A boolean indicating whether the generation process was successful. + details: A list of tuples containing detailed information about the generation process. + """ + success: bool - details: List[Tuple[str, bool, str]] + details: t.MutableSequence[t.Tuple[str, bool, str]] class CommandDTO(BaseModel): - id: Optional[str] + """ + This class represents a command. + + Attributes: + id: The unique identifier of the command. + action: The action to be performed by the command. + args: The arguments for the command action. + version: The version of the command. + """ + + id: t.Optional[str] action: str - # if args is a list, this mean the command will be mapped to the list of args - args: Union[List[JSON], JSON] + args: t.Union[t.MutableSequence[JSON], JSON] version: int = 1 class CommandResultDTO(BaseModel): + """ + This class represents the result of a command. + + Attributes: + study_id: The unique identifier of the study. + id: The unique identifier of the command. + success: A boolean indicating whether the command was successful. + message: A message detailing the result of the command. + """ + study_id: str id: str success: bool message: str -class VariantTreeDTO(BaseModel): - node: StudyMetadataDTO - children: List["VariantTreeDTO"] +class VariantTreeDTO: + """ + This class represents a variant tree structure. + Attributes: + node: The metadata of the study (ID, name, version, etc.). + children: A list of variant children. + """ -VariantTreeDTO.update_forward_refs() + def __init__(self, node: StudyMetadataDTO, children: t.MutableSequence["VariantTreeDTO"]) -> None: + # We are intentionally not using Pydantic’s `BaseModel` here to prevent potential + # `RecursionError` exceptions that can occur with Pydantic versions before v2. + self.node = node + self.children = children or [] diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 89da140393..977d760fb6 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,8 @@ v2.16.8 (unreleased) * **st-storages (ui):** correction of incorrect wording between "withdrawal" and "injection" [`#1977`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1977) * **st-storages (ui):** change matrix titles [`#1994`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1994) * **st-storages:** use command when updating matrices [`#1971`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1971) +* **variants:** avoid recursive error when creating big variant tree [`#1967`](https://github.com/AntaresSimulatorTeam/AntaREST/pull/1967) + v2.16.7 (2024-03-05) -------------------- diff --git a/tests/integration/studies_blueprint/test_synthesis.py b/tests/integration/studies_blueprint/test_synthesis.py index 70f5f0c907..aa6141e782 100644 --- a/tests/integration/studies_blueprint/test_synthesis.py +++ b/tests/integration/studies_blueprint/test_synthesis.py @@ -58,7 +58,7 @@ def test_raw_study( ) assert res.status_code == 200, res.json() duration = time.time() - start - assert 0 <= duration <= 0.1, f"Duration is {duration} seconds" + assert 0 <= duration <= 0.3, f"Duration is {duration} seconds" def test_variant_study( self, diff --git a/tests/integration/variant_blueprint/test_variant_manager.py b/tests/integration/variant_blueprint/test_variant_manager.py index 5af256dbbe..9d5be37e2b 100644 --- a/tests/integration/variant_blueprint/test_variant_manager.py +++ b/tests/integration/variant_blueprint/test_variant_manager.py @@ -186,3 +186,44 @@ def test_variant_manager(client: TestClient, admin_access_token: str, study_id: res = client.get(f"/v1/studies/{variant_id}", headers=admin_headers) assert res.status_code == 404 + + +def test_comments(client: TestClient, admin_access_token: str, variant_id: str) -> None: + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} + + # Put comments + comment = "updated comment" + res = client.put(f"/v1/studies/{variant_id}/comments", json={"comments": comment}, headers=admin_headers) + assert res.status_code == 204 + + # Asserts comments are updated + res = client.get(f"/v1/studies/{variant_id}/comments", headers=admin_headers) + assert res.json() == comment + + # Generates the study + res = client.put(f"/v1/studies/{variant_id}/generate?denormalize=false&from_scratch=true", headers=admin_headers) + task_id = res.json() + # Wait for task completion + res = client.get(f"/v1/tasks/{task_id}", headers=admin_headers, params={"wait_for_completion": True}) + assert res.status_code == 200 + task_result = TaskDTO.parse_obj(res.json()) + assert task_result.status == TaskStatus.COMPLETED + assert task_result.result is not None + assert task_result.result.success + + # Asserts comments did not disappear + res = client.get(f"/v1/studies/{variant_id}/comments", headers=admin_headers) + assert res.json() == comment + + +def test_recursive_variant_tree(client: TestClient, admin_access_token: str): + admin_headers = {"Authorization": f"Bearer {admin_access_token}"} + base_study_res = client.post("/v1/studies?name=foo", headers=admin_headers) + base_study_id = base_study_res.json() + parent_id = base_study_res.json() + for k in range(150): + res = client.post(f"/v1/studies/{base_study_id}/variants?name=variant_{k}", headers=admin_headers) + base_study_id = res.json() + # Asserts that we do not trigger a Recursive Exception + res = client.get(f"/v1/studies/{parent_id}/variants", headers=admin_headers) + assert res.status_code == 200