Skip to content

Commit

Permalink
refactor(export): request for Study export function (#1669)
Browse files Browse the repository at this point in the history
Co-authored-by: LAIDI Takfarinas (Externe) <[email protected]>
Co-authored-by: TLAIDI <[email protected]>
  • Loading branch information
3 people authored and laurent-laporte-pro committed Sep 12, 2023
1 parent 97768f6 commit a37cf1f
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 156 deletions.
24 changes: 3 additions & 21 deletions antarest/study/common/studystorage.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,27 +230,6 @@ def export_output(self, metadata: T, output_id: str, target: Path) -> None:
"""
raise NotImplementedError()

@abstractmethod
def export_study_flat(
self,
metadata: T,
dst_path: Path,
outputs: bool = True,
output_list_filter: Optional[List[str]] = None,
denormalize: bool = True,
) -> None:
"""
Export study to destination
Args:
metadata: study.
dst_path: destination path.
outputs: list of outputs to keep.
output_list_filter: list of outputs to keep (None indicate all outputs).
denormalize: denormalize the study (replace matrix links by real matrices).
"""
raise NotImplementedError()

@abstractmethod
def get_synthesis(self, metadata: T, params: Optional[RequestParameters] = None) -> FileStudyTreeConfigDTO:
"""
Expand All @@ -274,3 +253,6 @@ def archive_study_output(self, study: T, output_id: str) -> bool:
@abstractmethod
def unarchive_study_output(self, study: T, output_id: str, keep_src_zip: bool) -> bool:
raise NotImplementedError()

def unarchive(self, study: T) -> None:
raise NotImplementedError()
39 changes: 35 additions & 4 deletions antarest/study/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import io
import json
import logging
import shutil
import os
from datetime import datetime, timedelta
from http import HTTPStatus
Expand Down Expand Up @@ -106,6 +107,7 @@
remove_from_cache,
study_matcher,
)
from antarest.study.storage.abstract_storage_service import export_study_flat
from antarest.study.storage.variantstudy.model.command.icommand import ICommand
from antarest.study.storage.variantstudy.model.command.replace_matrix import ReplaceMatrix
from antarest.study.storage.variantstudy.model.command.update_comments import UpdateComments
Expand Down Expand Up @@ -918,7 +920,17 @@ def export_study(
def export_task(notifier: TaskUpdateNotifier) -> TaskResult:
try:
target_study = self.get_study(uuid)
self.storage_service.get_storage(target_study).export_study(target_study, export_path, outputs)
storage = self.storage_service.get_storage(target_study)
if isinstance(target_study, RawStudy):
if target_study.archived:
storage.unarchive(target_study)
try:
storage.export_study(target_study, export_path, outputs)
finally:
if target_study.archived:
shutil.rmtree(target_study.path)
else:
storage.export_study(target_study, export_path, outputs)
self.file_transfer_manager.set_ready(export_id)
return TaskResult(success=True, message=f"Study {uuid} successfully exported")
except Exception as e:
Expand Down Expand Up @@ -1020,9 +1032,28 @@ def export_study_flat(
study = self.get_study(uuid)
assert_permission(params.user, study, StudyPermissionType.READ)
self._assert_study_unarchived(study)

return self.storage_service.get_storage(study).export_study_flat(
study, dest, len(output_list or []) > 0, output_list
path_study = Path(study.path)
if isinstance(study, RawStudy):
if study.archived:
self.storage_service.get_storage(study).unarchive(study)
try:
return export_study_flat(
path_study=path_study,
dest=dest,
outputs=len(output_list or []) > 0,
output_list_filter=output_list,
)
finally:
if study.archived:
shutil.rmtree(study.path)
snapshot_path = path_study / "snapshot"
output_src_path = path_study / "output"
export_study_flat(
path_study=snapshot_path,
dest=dest,
outputs=len(output_list or []) > 0,
output_list_filter=output_list,
output_src_path=output_src_path,
)

def delete_study(self, uuid: str, children: bool, params: RequestParameters) -> None:
Expand Down
69 changes: 68 additions & 1 deletion antarest/study/storage/abstract_storage_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
from pathlib import Path
from typing import IO, List, Optional, Union
from uuid import uuid4
import time
from zipfile import ZipFile
import os


from antarest.core.config import Config
from antarest.core.exceptions import BadOutputError, StudyOutputNotFoundError
Expand Down Expand Up @@ -32,6 +36,60 @@
logger = logging.getLogger(__name__)


def export_study_flat(
path_study: Path,
dest: Path,
outputs: bool = True,
output_list_filter: Optional[List[str]] = None,
output_src_path: Optional[Path] = None,
) -> None:
"""
Export study to destination
Args:
path_study: Study source path
dest: Destination path.
outputs: List of outputs to keep.
output_list_filter: List of outputs to keep (None indicate all outputs).
output_src_path: Denormalize the study (replace matrix links by real matrices).
"""
start_time = time.time()
output_src_path = output_src_path or path_study / "output"
output_dest_path = dest / "output"
ignore_patterns = (
lambda directory, contents: ["output"]
if str(directory) == str(path_study)
else []
)

shutil.copytree(src=path_study, dst=dest, ignore=ignore_patterns)
if outputs and output_src_path.is_dir():
if output_dest_path.exists():
shutil.rmtree(output_dest_path)
if output_list_filter is not None:
os.mkdir(output_dest_path)
for output in output_list_filter:
zip_path = output_src_path / f"{output}.zip"
if zip_path.exists():
with ZipFile(zip_path) as zf:
zf.extractall(output_dest_path / output)
else:
shutil.copytree(
src=output_src_path / output,
dst=output_dest_path / output,
)
else:
shutil.copytree(
src=output_src_path,
dst=output_dest_path,
)

stop_time = time.time()
duration = "{:.3f}".format(stop_time - start_time)
logger.info(f"Study {path_study} exported (flat mode) in {duration}s")


class AbstractStorageService(IStudyStorageService[T], ABC):
def __init__(
self,
Expand Down Expand Up @@ -230,7 +288,16 @@ def export_study(self, metadata: T, target: Path, outputs: bool = True) -> Path:
with tempfile.TemporaryDirectory(dir=self.config.storage.tmp_dir) as tmpdir:
logger.info(f"Exporting study {metadata.id} to temporary path {tmpdir}")
tmp_study_path = Path(tmpdir) / "tmp_copy"
self.export_study_flat(metadata, tmp_study_path, outputs)
if not isinstance(metadata, RawStudy):
snapshot_path = path_study / "snapshot"
output_src_path = path_study / "output"
export_study_flat(
path_study=snapshot_path,
dest=tmp_study_path,
outputs=outputs,
output_src_path=output_src_path,
)
export_study_flat(path_study, tmp_study_path, outputs)
stopwatch = StopWatch()
zip_dir(tmp_study_path, target)
stopwatch.log_elapsed(lambda x: logger.info(f"Study {path_study} exported (zipped mode) in {x}s"))
Expand Down
26 changes: 0 additions & 26 deletions antarest/study/storage/rawstudy/raw_study_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from antarest.study.storage.rawstudy.model.filesystem.lazy_node import LazyNode
from antarest.study.storage.utils import (
create_new_empty_study,
export_study_flat,
fix_study_root,
get_default_workspace_path,
is_managed,
Expand Down Expand Up @@ -323,31 +322,6 @@ def import_study(self, metadata: RawStudy, stream: IO[bytes]) -> Study:
metadata.path = str(path_study)
return metadata

def export_study_flat(
self,
metadata: RawStudy,
dst_path: Path,
outputs: bool = True,
output_list_filter: Optional[List[str]] = None,
denormalize: bool = True,
) -> None:
path_study = Path(metadata.path)

if metadata.archived:
self.unarchive(metadata)
try:
export_study_flat(
path_study,
dst_path,
self.study_factory,
outputs,
output_list_filter,
denormalize,
)
finally:
if metadata.archived:
shutil.rmtree(metadata.path)

def check_errors(
self,
metadata: RawStudy,
Expand Down
47 changes: 0 additions & 47 deletions antarest/study/storage/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,50 +326,3 @@ def get_start_date(
first_week_size=first_week_size,
level=level,
)


def export_study_flat(
path_study: Path,
dest: Path,
study_factory: StudyFactory,
outputs: bool = True,
output_list_filter: Optional[List[str]] = None,
denormalize: bool = True,
output_src_path: Optional[Path] = None,
) -> None:
start_time = time.time()

output_src_path = output_src_path or path_study / "output"
output_dest_path = dest / "output"
ignore_patterns = lambda directory, contents: ["output"] if str(directory) == str(path_study) else []

shutil.copytree(src=path_study, dst=dest, ignore=ignore_patterns)

if outputs and output_src_path.exists():
if output_list_filter is None:
# Retrieve all directories or ZIP files without duplicates
output_list_filter = list(
{f.with_suffix("").name for f in output_src_path.iterdir() if f.is_dir() or f.suffix == ".zip"}
)
# Copy each folder or uncompress each ZIP file to the destination dir.
shutil.rmtree(output_dest_path, ignore_errors=True)
output_dest_path.mkdir()
for output in output_list_filter:
zip_path = output_src_path / f"{output}.zip"
if zip_path.exists():
with ZipFile(zip_path) as zf:
zf.extractall(output_dest_path / output)
else:
shutil.copytree(
src=output_src_path / output,
dst=output_dest_path / output,
)

stop_time = time.time()
duration = "{:.3f}".format(stop_time - start_time)
logger.info(f"Study {path_study} exported (flat mode) in {duration}s")
study = study_factory.create_from_fs(dest, "", use_cache=False)
if denormalize:
study.tree.denormalize()
duration = "{:.3f}".format(time.time() - stop_time)
logger.info(f"Study {path_study} denormalized in {duration}s")
56 changes: 20 additions & 36 deletions antarest/study/storage/variantstudy/variant_study_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@
from antarest.core.utils.utils import assert_this, suppress_exception
from antarest.matrixstore.service import MatrixService
from antarest.study.model import RawStudy, Study, StudyAdditionalData, StudyMetadataDTO, StudySimResultDTO
from antarest.study.storage.abstract_storage_service import AbstractStorageService
from antarest.study.storage.abstract_storage_service import AbstractStorageService, export_study_flat
from antarest.study.storage.patch_service import PatchService
from antarest.study.storage.rawstudy.model.filesystem.config.model import FileStudyTreeConfig, FileStudyTreeConfigDTO
from antarest.study.storage.rawstudy.model.filesystem.factory import FileStudy, StudyFactory
from antarest.study.storage.rawstudy.raw_study_service import RawStudyService
from antarest.study.storage.utils import (
assert_permission,
export_study_flat,
get_default_workspace_path,
is_managed,
remove_from_cache,
Expand Down Expand Up @@ -719,22 +718,30 @@ def _generate(
last_executed_command_index = None

if last_executed_command_index is None:
# Copy parent study to destination
if isinstance(parent_study, VariantStudy):
self._safe_generation(parent_study)
self.export_study_flat(
metadata=parent_study,
dst_path=dst_path,
path_study = Path(parent_study.path)
snapshot_path = path_study / SNAPSHOT_RELATIVE_PATH
output_src_path = path_study / "output"
export_study_flat(
snapshot_path,
dst_path,
outputs=False,
denormalize=False,
output_src_path=output_src_path,
)
else:
self.raw_study_service.export_study_flat(
metadata=parent_study,
dst_path=dst_path,
outputs=False,
denormalize=False,
)
path_study = Path(parent_study.path)
if parent_study.archived:
self.raw_study_service.unarchive(parent_study)
try:
export_study_flat(
path_study=path_study,
dest=dst_path,
outputs=False,
)
finally:
if parent_study.archived:
shutil.rmtree(parent_study.path)

command_start_index = last_executed_command_index + 1 if last_executed_command_index is not None else 0
logger.info(f"Generating study snapshot from command index {command_start_index}")
Expand Down Expand Up @@ -1068,29 +1075,6 @@ def get_study_path(self, metadata: Study) -> Path:
"""
return Path(metadata.path) / SNAPSHOT_RELATIVE_PATH

def export_study_flat(
self,
metadata: VariantStudy,
dst_path: Path,
outputs: bool = True,
output_list_filter: Optional[List[str]] = None,
denormalize: bool = True,
) -> None:
self._safe_generation(metadata)
path_study = Path(metadata.path)

snapshot_path = path_study / SNAPSHOT_RELATIVE_PATH
output_src_path = path_study / "output"
export_study_flat(
snapshot_path,
dst_path,
self.study_factory,
outputs,
output_list_filter,
denormalize,
output_src_path,
)

def get_synthesis(
self,
metadata: VariantStudy,
Expand Down
10 changes: 4 additions & 6 deletions tests/storage/business/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from antarest.core.config import Config, StorageConfig
from antarest.study.model import DEFAULT_WORKSPACE_NAME, RawStudy
from antarest.study.storage.rawstudy.raw_study_service import RawStudyService
from antarest.study.storage.abstract_storage_service import export_study_flat


@pytest.mark.unit_test
Expand Down Expand Up @@ -104,17 +105,14 @@ def test_export_flat(tmp_path: Path):
study_factory.create_from_fs.return_value = study_tree

study = RawStudy(id="id", path=root)
path_study = Path(study.path)

study_service.export_study_flat(study, tmp_path / "copy_with_output", outputs=True)

export_study_flat(path_study, tmp_path / "copy_with_output", outputs=True)
copy_with_output_hash = dirhash(tmp_path / "copy_with_output", "md5")

assert root_hash == copy_with_output_hash

study_service.export_study_flat(study, tmp_path / "copy_without_output", outputs=False)

export_study_flat(path_study, tmp_path / "copy_without_output", outputs=False)
copy_without_output_hash = dirhash(tmp_path / "copy_without_output", "md5")

assert root_without_output_hash == copy_without_output_hash


Expand Down
Loading

0 comments on commit a37cf1f

Please sign in to comment.