From d57e61560fcdfdf13a22815d9846b916f1cdc7bb Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 15:20:23 +0200 Subject: [PATCH 01/17] use move to simplify --- antarest/output/storage/file/abstract_storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/antarest/output/storage/file/abstract_storage.py b/antarest/output/storage/file/abstract_storage.py index 0d151f50b2..c94cd88fdd 100644 --- a/antarest/output/storage/file/abstract_storage.py +++ b/antarest/output/storage/file/abstract_storage.py @@ -189,7 +189,7 @@ def _import_zip_as_archived( output_full_name = extract_output_name(output_zip_path, output_name_suffix) final_path = _archived_output_path(study_outputs_path, output_full_name) study_outputs_path.mkdir(exist_ok=True) - shutil.copyfile(output_zip_path, final_path) + shutil.move(output_zip_path, final_path) _add_logs(final_path, logs) From 8d872381c25d94cb45ed07483c8eed9799fe6544 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 15:22:33 +0200 Subject: [PATCH 02/17] mark field as deprecated --- antarest/launcher/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/antarest/launcher/model.py b/antarest/launcher/model.py index 85225cd8d6..9106c703eb 100644 --- a/antarest/launcher/model.py +++ b/antarest/launcher/model.py @@ -81,7 +81,7 @@ class LauncherParametersDTO(AntaresBaseModel, extra="forbid"): time_limit: int = 240 * 3600 # Default value set to 240 hours (in seconds) xpansion: XpansionParametersDTO | bool | None = None xpansion_r_version: bool = False - archive_output: bool = True + archive_output: bool = Field(deprecated=True, default=True) auto_unzip: bool = True output_suffix: FileNameStr | None = None other_options: str | None = None From a0272100c1b1b2a9a24f18bea85a9a05a4db70f3 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 15:27:56 +0200 Subject: [PATCH 03/17] continue --- antarest/launcher/service.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 6a4ab54d13..cdaed24736 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -64,6 +64,7 @@ from antarest.login.service import LoginService from antarest.login.utils import current_user_context, get_current_user, require_current_user from antarest.output.service import OutputService +from antarest.study.model import Study from antarest.study.repository import AccessPermissions, StudyFilter from antarest.study.service import StudyService from antarest.study.storage.utils import assert_permission, extract_output_name, find_single_output_path @@ -532,12 +533,17 @@ def _import_output( job_result = self.job_result_repository.get(job_id) if not job_result: raise JobNotFound() - study_id = job_result.study_id + job_owner_id = job_result.owner_id job_launch_params = LauncherParametersDTO.from_launcher_params(job_result.launcher_params) output_true_path = find_single_output_path(output_path) + study_id = job_result.study_id + study = db.session.get(Study, study_id) + if study is None: + return self._import_fallback_output(job_id, output_true_path, job_launch_params.output_suffix) + if not output_true_path.is_dir() and not is_zip(output_true_path): raise NoValidOutputError(f"No valid output for job {job_id}: {output_true_path}") @@ -575,11 +581,7 @@ def _import_output( logs=additional_logs, ) except StudyNotFoundError: - return self._import_fallback_output( - job_id, - final_output_path, - job_launch_params.output_suffix, - ) + return self._import_fallback_output(job_id, final_output_path, job_launch_params.output_suffix) finally: # Delete the temporary zip file, which now has been imported if zip_path: From 421d9f52ae347489ac5132f4ff3f7fb3c7dad918 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 15:31:02 +0200 Subject: [PATCH 04/17] do not rezip for managed studies --- antarest/launcher/service.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index cdaed24736..430f720a7d 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -67,7 +67,7 @@ from antarest.study.model import Study from antarest.study.repository import AccessPermissions, StudyFilter from antarest.study.service import StudyService -from antarest.study.storage.utils import assert_permission, extract_output_name, find_single_output_path +from antarest.study.storage.utils import assert_permission, extract_output_name, find_single_output_path, is_managed logger = logging.getLogger(__name__) @@ -538,23 +538,22 @@ def _import_output( job_launch_params = LauncherParametersDTO.from_launcher_params(job_result.launcher_params) output_true_path = find_single_output_path(output_path) + if not output_true_path.is_dir() and not is_zip(output_true_path): + raise NoValidOutputError(f"No valid output for job {job_id}: {output_true_path}") study_id = job_result.study_id study = db.session.get(Study, study_id) if study is None: return self._import_fallback_output(job_id, output_true_path, job_launch_params.output_suffix) - - if not output_true_path.is_dir() and not is_zip(output_true_path): - raise NoValidOutputError(f"No valid output for job {job_id}: {output_true_path}") + is_study_managed = is_managed(study) self._save_solver_stats(job_result, output_true_path) zip_path: Path | None = None - # Optimized path for studies stored on external devices, that will then be unarchived there. - # TODO: that whole optimization path should be refactored to: - # - be more explicit - # - not affect internal studies - if job_launch_params.archive_output: + if not is_study_managed: + # Optimized path for studies stored on external devices, that will then be unarchived there. + # TODO: that whole optimization path should be refactored to: + # - be more explicit stopwatch = StopWatch() logger.info("Re zipping output for transfer") zip_path = output_true_path.parent / f"{output_true_path.name}.zip" From 2a0194a4c7d65aca8262c14e6b321d8975c32b44 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 15:40:47 +0200 Subject: [PATCH 05/17] perfs(output): avoid rezipping output for managed studies --- antarest/output/service.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/antarest/output/service.py b/antarest/output/service.py index b78f86b4a8..9809c1ba93 100644 --- a/antarest/output/service.py +++ b/antarest/output/service.py @@ -412,8 +412,12 @@ def import_output( # Optimized path for studies stored on external devices, that will then be unarchived there. # TODO: as commented elsewhere, that workflow should be refactored to not span multiple files - if output_id and isinstance(output, Path) and output.suffix == ArchiveFormat.ZIP and auto_unzip: - self.unarchive_output(uuid, output_id) + if output_id and isinstance(output, Path): + if output.suffix == ArchiveFormat.ZIP and auto_unzip: + self.unarchive_output(uuid, output_id) + if output.is_dir() and not auto_unzip: + # This only happens for managed studies (not obvious due to the leaky workflow) + self.archive_output(uuid, output_id) return output_id From 63f767c5a6fa5accee1ced40d4babb2783e2327f Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:00:21 +0200 Subject: [PATCH 06/17] c --- tests/output/storage/file/test_file_output_storage.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/output/storage/file/test_file_output_storage.py b/tests/output/storage/file/test_file_output_storage.py index 3514fd0378..16def0f21b 100644 --- a/tests/output/storage/file/test_file_output_storage.py +++ b/tests/output/storage/file/test_file_output_storage.py @@ -547,9 +547,7 @@ def test_import_output_directory(output_storage: IOutputStorage, tmp_path: Path) assert output_storage.get_logs("my-study", f"{expected_date}eco-other", LogType.STDERR) == "some error" -def test_import_output_zip_should_import_it_as_archived( - output_storage: IOutputStorage, tmp_path: Path, sta_mini_zip_path: Path -) -> None: +def test_import_output_zip_should_import_it_as_archived(output_storage: IOutputStorage, tmp_path: Path) -> None: # Checks the "optimized path" for zipped outputs, see TODOs # Use the `20201014-1430adq-2` output as it's already zipped From 7fd07f3c79b7ea3db9874a4e95fc3c8a7694b2a6 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:01:42 +0200 Subject: [PATCH 07/17] fix test --- tests/output/storage/file/test_file_output_storage.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/output/storage/file/test_file_output_storage.py b/tests/output/storage/file/test_file_output_storage.py index 16def0f21b..04509d6843 100644 --- a/tests/output/storage/file/test_file_output_storage.py +++ b/tests/output/storage/file/test_file_output_storage.py @@ -562,6 +562,10 @@ def test_import_output_zip_should_import_it_as_archived(output_storage: IOutputS zip_path = outputs_dir / "STA-mini" / "20201014-1430adq-2.zip" in_study = False + # Copies the `zip_path` as `import_output` cleans it. + second_zip_path = tmp_path / "output_copied.zip" + shutil.copy(zip_path, second_zip_path) + # Import zip file output_id = output_storage.import_output("my-study", zip_path) @@ -579,7 +583,7 @@ def test_import_output_zip_should_import_it_as_archived(output_storage: IOutputS err_logs = tmp_path / "err.log" err_logs.write_text("some error") output_id = output_storage.import_output( - "my-study", zip_path, output_name_suffix="other", logs=SimulationLogs(out_logs, err_logs) + "my-study", second_zip_path, output_name_suffix="other", logs=SimulationLogs(out_logs, err_logs) ) assert output_id == f"{expected_date}adq-other" assert output_storage.list_outputs("my-study") == [ From 617b02534756347135426267d365eecaff6618b2 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:07:58 +0200 Subject: [PATCH 08/17] fixing another test --- tests/launcher/test_service.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index 1c9bf9a68e..20e1ccab6f 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -13,6 +13,7 @@ import json import os import time +import uuid from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import Mock, call @@ -39,7 +40,7 @@ from antarest.core.model import PermissionInfo, PublicMode from antarest.core.requests import UserHasNotPermissionError from antarest.core.utils.fastapi_sqlalchemy import DBSessionMiddleware -from antarest.core.utils.fastapi_sqlalchemy.middleware import init_db_singleton +from antarest.core.utils.fastapi_sqlalchemy.middleware import db, init_db_singleton from antarest.core.utils.utils import current_time from antarest.dbmodel import Base from antarest.launcher.adapters.abstractlauncher import SimulationLogs @@ -83,7 +84,7 @@ from antarest.study.storage.variantstudy.command_factory import CommandFactory from antarest.study.storage.variantstudy.model.command_context import CommandContext from antarest.study.storage.variantstudy.variant_study_service import VariantStudyService -from tests.helpers import with_admin_user +from tests.helpers import create_raw_study, with_admin_user, with_db_context class TestLauncherService: @@ -1002,6 +1003,7 @@ def test_save_solver_stats(self, tmp_path: Path) -> None: ) assert actual_obj.to_dto().model_dump() == expected_obj.to_dto().model_dump() + @with_db_context def test_import_output_is_called_with_the_right_user(self, tmp_path: Path) -> None: # Create user jwt_user = JWTUser(id=2, impersonator=2, type="users") @@ -1009,9 +1011,14 @@ def test_import_output_is_called_with_the_right_user(self, tmp_path: Path) -> No login_service = Mock() login_service.get_jwt.return_value = jwt_user # Put this user as the job owner - job_result = JobResult(study_id="study_id", owner_id=jwt_user.id) + study_id = str(uuid.uuid4()) + job_result = JobResult(study_id=study_id, owner_id=jwt_user.id) job_repository = Mock() job_repository.get.return_value = job_result + # Adds the study to DB + study = create_raw_study(study_id, "study-test", tmp_path) + db.session.add(study) + db.session.commit() # fake import_output function that checks the current user def fake_import_output( @@ -1039,6 +1046,7 @@ def fake_import_output( # Ensures the output_service.import_output method was called with the right user launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) + launcher_service.output_service.import_output.assert_called_once() @with_admin_user def test_run_study_with_solver_presets(self) -> None: From d13cd9a31fbcd939e7bb197281f974c69b913884 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:10:54 +0200 Subject: [PATCH 09/17] fix the other failing test --- tests/launcher/test_service.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index 20e1ccab6f..f20e5cd056 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -777,6 +777,7 @@ def test_get_logs(self, tmp_path: Path) -> None: ) @with_admin_user + @with_db_context def test_manage_output(self, tmp_path: Path) -> None: # TODO: finish adaptation study_service = Mock() @@ -813,7 +814,12 @@ def test_manage_output(self, tmp_path: Path) -> None: output_data.writestr("some output", "0\n1") job_id = "job_id" zipped_job_id = "zipped_job_id" - study_id = "study_id" + study_id = str(uuid.uuid4()) + # Adds the study linked to the job inside DB + study = create_raw_study(study_id, "study-test", tmp_path) + db.session.add(study) + db.session.commit() + # Defines the side effects launcher_service.job_result_repository.get.side_effect = [ None, JobResult(id=job_id, study_id=study_id), @@ -1015,7 +1021,7 @@ def test_import_output_is_called_with_the_right_user(self, tmp_path: Path) -> No job_result = JobResult(study_id=study_id, owner_id=jwt_user.id) job_repository = Mock() job_repository.get.return_value = job_result - # Adds the study to DB + # Adds the study linked to the job inside DB study = create_raw_study(study_id, "study-test", tmp_path) db.session.add(study) db.session.commit() From d6c6c6748ba599d1305db12067bf31b73e8b5c13 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:15:13 +0200 Subject: [PATCH 10/17] clean test --- tests/integration/variant_blueprint/test_variant_manager.py | 6 +----- tests/launcher/test_service.py | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/integration/variant_blueprint/test_variant_manager.py b/tests/integration/variant_blueprint/test_variant_manager.py index 79db7a88cb..f3a953745a 100644 --- a/tests/integration/variant_blueprint/test_variant_manager.py +++ b/tests/integration/variant_blueprint/test_variant_manager.py @@ -372,11 +372,7 @@ def test_outputs(client: TestClient, admin_access_token: str, variant_id: str, t def test_clear_snapshots( - client: TestClient, - admin_access_token: str, - tmp_path: Path, - generate_snapshots: t.List[str], - monkeypatch: pytest.MonkeyPatch, + client: TestClient, admin_access_token: str, tmp_path: Path, generate_snapshots: list[str] ) -> None: """ The `snapshot/` directory must not exist after a call to `clear-snapshot`. diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index f20e5cd056..e81983c8b9 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -1154,6 +1154,9 @@ def get_mock_solver_presets(solver_presets_id: str): with pytest.raises(IncompatibleSolverPresets): launcher_service.run_study("study_uuid", "local", params_with_other_options, "config-1", "8.0") + def test_import_output_for_different_workspaces(self) -> None: + pass + class TestNormalizeScheduledAt: NOW = datetime(2026, 7, 7, 12, 0, 0) From f3df253b240ec630da7af3cd1591d504677297c1 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:16:51 +0200 Subject: [PATCH 11/17] remove another monkeypatch --- tests/integration/variant_blueprint/test_variant_manager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration/variant_blueprint/test_variant_manager.py b/tests/integration/variant_blueprint/test_variant_manager.py index f3a953745a..ba618e993e 100644 --- a/tests/integration/variant_blueprint/test_variant_manager.py +++ b/tests/integration/variant_blueprint/test_variant_manager.py @@ -49,9 +49,7 @@ def variant_id_fixture(client: TestClient, admin_access_token: str, base_study_i @pytest.fixture(name="generate_snapshots") -def generate_snapshot_fixture( - client: TestClient, admin_access_token: str, base_study_id: str, monkeypatch: pytest.MonkeyPatch -) -> t.List[str]: +def generate_snapshot_fixture(client: TestClient, admin_access_token: str, base_study_id: str) -> list[str]: """Generate some snapshots with different date of update and last access""" # Initialize variant_ids list From c5d9432739d8507c5379cfe975371c94b0fac0ef Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:17:20 +0200 Subject: [PATCH 12/17] another monkeypatch --- tests/study/storage/variantstudy/test_variant_study_service.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/study/storage/variantstudy/test_variant_study_service.py b/tests/study/storage/variantstudy/test_variant_study_service.py index 5708dc7362..40cbcd105c 100644 --- a/tests/study/storage/variantstudy/test_variant_study_service.py +++ b/tests/study/storage/variantstudy/test_variant_study_service.py @@ -220,7 +220,6 @@ def test_clear_all_snapshots( variant_study_service: VariantStudyService, raw_study_service: RawStudyService, fs_dao: FileStudyTreeDao, - monkeypatch: pytest.MonkeyPatch, ) -> None: """ - Test return value in case the user is not allowed to call the function, From 4c4524005deb41e8cd8a3f1a464b51d35245216b Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:39:25 +0200 Subject: [PATCH 13/17] start writing test --- tests/launcher/test_service.py | 48 ++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index e81983c8b9..6fafeac421 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -71,6 +71,7 @@ from antarest.login.utils import current_user_context, get_current_user from antarest.output.service import OutputService from antarest.study.model import ( + DEFAULT_WORKSPACE_NAME, STUDY_VERSION_8_8, STUDY_VERSION_9_2, OwnerInfo, @@ -1154,8 +1155,51 @@ def get_mock_solver_presets(solver_presets_id: str): with pytest.raises(IncompatibleSolverPresets): launcher_service.run_study("study_uuid", "local", params_with_other_options, "config-1", "8.0") - def test_import_output_for_different_workspaces(self) -> None: - pass + @with_db_context + @pytest.mark.parametrize("managed", [True, False]) + def test_import_output_for_different_workspaces(self, tmp_path: Path, managed: bool) -> None: + ########################## + # Set Up + ########################## + + # Create a study in DB + study_id = str(uuid.uuid4()) + study = create_raw_study(study_id, "study-test", path=str(tmp_path)) + if managed: + study.workspace = DEFAULT_WORKSPACE_NAME + else: + study.workspace = "other-workspace" + db.session.add(study) + db.session.commit() + + # Create a fake job + job_result = JobResult(study_id=study_id, owner_id=1) + job_repository = Mock() + job_repository.get.return_value = job_result + + # Builds the service + output_service = Mock() + output_service.import_output.side_effect = None + launcher_service = LauncherService( + config=Mock(), + study_service=Mock(), + output_service=output_service, + login_service=Mock(), + job_result_repository=job_repository, + solver_presets_repository=Mock(), + event_bus=Mock(), + factory_launcher=Mock(), + file_transfer_manager=Mock(), + task_service=Mock(), + cache=Mock(), + ) + + ########################## + # Test + ########################## + + # todo: We should test that if it is managed we do not go through archive_dir. + launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) class TestNormalizeScheduledAt: From a346b6e88956d5aaeb7685aeeab3fc280812f1a6 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:41:42 +0200 Subject: [PATCH 14/17] c --- tests/launcher/test_service.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index 6fafeac421..11477cf0d4 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -16,7 +16,7 @@ import uuid from datetime import datetime, timedelta, timezone from pathlib import Path -from unittest.mock import Mock, call +from unittest.mock import Mock, call, patch from uuid import uuid4 from zipfile import ZIP_DEFLATED, ZipFile @@ -1164,7 +1164,7 @@ def test_import_output_for_different_workspaces(self, tmp_path: Path, managed: b # Create a study in DB study_id = str(uuid.uuid4()) - study = create_raw_study(study_id, "study-test", path=str(tmp_path)) + study = create_raw_study(study_id, "study-test", path="") if managed: study.workspace = DEFAULT_WORKSPACE_NAME else: @@ -1199,7 +1199,8 @@ def test_import_output_for_different_workspaces(self, tmp_path: Path, managed: b ########################## # todo: We should test that if it is managed we do not go through archive_dir. - launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) + with patch("os.scandir", side_effect=FileNotFoundError("File doesn't exist")): + launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) class TestNormalizeScheduledAt: From b39ccfdca77b445675beef2c3a76d7533f2b8432 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Tue, 25 Aug 2026 16:45:04 +0200 Subject: [PATCH 15/17] finalize homemade test --- tests/launcher/test_service.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/launcher/test_service.py b/tests/launcher/test_service.py index 11477cf0d4..981adf385e 100644 --- a/tests/launcher/test_service.py +++ b/tests/launcher/test_service.py @@ -1198,9 +1198,16 @@ def test_import_output_for_different_workspaces(self, tmp_path: Path, managed: b # Test ########################## - # todo: We should test that if it is managed we do not go through archive_dir. - with patch("os.scandir", side_effect=FileNotFoundError("File doesn't exist")): - launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) + # We patch the `archive_dir` function to always raise. + # This way we can check if it was called or not. + with patch("antarest.launcher.service.archive_dir", side_effect=ValueError("Output archiving failed for test")): + if managed: + # We should not raise here as we do not need to archive the output. + launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) + else: + # Here we expect the method to raise as we need to archive the output in order to unarchive it later on the Windows VM. + with pytest.raises(ValueError, match="Output archiving failed for test"): + launcher_service._import_output("job_id", tmp_path, SimulationLogs.no_logs()) class TestNormalizeScheduledAt: From 4a9eb2507a6b0ec065d861411510fdb8784f88c3 Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 27 Aug 2026 14:05:54 +0200 Subject: [PATCH 16/17] resolve comments --- antarest/launcher/service.py | 6 +++--- antarest/output/storage/file/abstract_storage.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 430f720a7d..25cf0464fd 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -541,14 +541,14 @@ def _import_output( if not output_true_path.is_dir() and not is_zip(output_true_path): raise NoValidOutputError(f"No valid output for job {job_id}: {output_true_path}") + self._save_solver_stats(job_result, output_true_path) + study_id = job_result.study_id study = db.session.get(Study, study_id) if study is None: return self._import_fallback_output(job_id, output_true_path, job_launch_params.output_suffix) is_study_managed = is_managed(study) - self._save_solver_stats(job_result, output_true_path) - zip_path: Path | None = None if not is_study_managed: # Optimized path for studies stored on external devices, that will then be unarchived there. @@ -584,7 +584,7 @@ def _import_output( finally: # Delete the temporary zip file, which now has been imported if zip_path: - os.unlink(zip_path) + zip_path.unlink(missing_ok=True) def _download_fallback_output(self, job_id: str) -> FileDownloadTaskDTO: output_path = self._get_job_output_fallback_path(job_id) diff --git a/antarest/output/storage/file/abstract_storage.py b/antarest/output/storage/file/abstract_storage.py index c94cd88fdd..0ad1284e5d 100644 --- a/antarest/output/storage/file/abstract_storage.py +++ b/antarest/output/storage/file/abstract_storage.py @@ -182,7 +182,7 @@ def _output_exists(outputs_root: Path, output_id: str) -> bool: def _import_zip_as_archived( study_id: str, output_zip_path: Path, study_outputs_path: Path, output_name_suffix: str | None, logs: SimulationLogs ) -> str: - """Simply copies the zip to destination study/output/.zip, with the right name extracted from output + """Simply moves the zip to destination study/output/.zip, with the right name extracted from output files.""" t = StopWatch() From fcf606e4462a4dea25fbef8b9706a7c9ada6afdd Mon Sep 17 00:00:00 2001 From: belthlemar Date: Thu, 27 Aug 2026 15:53:22 +0200 Subject: [PATCH 17/17] resolve last anis comment --- antarest/launcher/service.py | 29 ++++++++++++++++++----------- antarest/output/service.py | 11 ++++------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/antarest/launcher/service.py b/antarest/launcher/service.py index 25cf0464fd..b4ded6dae5 100644 --- a/antarest/launcher/service.py +++ b/antarest/launcher/service.py @@ -550,19 +550,26 @@ def _import_output( is_study_managed = is_managed(study) zip_path: Path | None = None - if not is_study_managed: - # Optimized path for studies stored on external devices, that will then be unarchived there. - # TODO: that whole optimization path should be refactored to: - # - be more explicit - stopwatch = StopWatch() - logger.info("Re zipping output for transfer") - zip_path = output_true_path.parent / f"{output_true_path.name}.zip" - archive_dir(output_true_path, target_archive_path=zip_path, archive_format=ArchiveFormat.ZIP) - logger.info(f"Zipped output for job {job_id} in {stopwatch}s") - final_output_path = zip_path - else: + + if is_zip(output_true_path): + # Possible if the option `-z` was used to run the solver. final_output_path = output_true_path + else: + if is_study_managed and job_launch_params.auto_unzip: + # Nothing to do, the output is already unarchived. + final_output_path = output_true_path + else: + # For studies stored on external devices, it's faster to re-zip the output for transfer and unarchive it there. + # Also, for managed studies when the user did not ask for auto-unzip, we'd better re-zip the output here instead of copying the tree and then zip it. + # TODO: that whole optimization should be refactored to be more explicit + stopwatch = StopWatch() + logger.info("Re zipping output for transfer") + zip_path = output_true_path.parent / f"{output_true_path.name}.zip" + archive_dir(output_true_path, target_archive_path=zip_path, archive_format=ArchiveFormat.ZIP) + logger.info(f"Zipped output for job {job_id} in {stopwatch}s") + final_output_path = zip_path + with db(): try: if job_owner_id: diff --git a/antarest/output/service.py b/antarest/output/service.py index 9809c1ba93..c36e521b35 100644 --- a/antarest/output/service.py +++ b/antarest/output/service.py @@ -410,14 +410,11 @@ def import_output( logger.info(f"output added to study {uuid}") - # Optimized path for studies stored on external devices, that will then be unarchived there. # TODO: as commented elsewhere, that workflow should be refactored to not span multiple files - if output_id and isinstance(output, Path): - if output.suffix == ArchiveFormat.ZIP and auto_unzip: - self.unarchive_output(uuid, output_id) - if output.is_dir() and not auto_unzip: - # This only happens for managed studies (not obvious due to the leaky workflow) - self.archive_output(uuid, output_id) + if output_id and isinstance(output, Path) and output.suffix == ArchiveFormat.ZIP and auto_unzip: + # Always the case for studies stored on external devices, as they will be unarchived there for performance reasons. + # It is also possible for managed studies if the option `-z` was used to run the solver. + self.unarchive_output(uuid, output_id) return output_id