pltobing's picture
chore(repo): finalize licensing and documentation
51e84d2
Raw History Blame Contribute Delete
4 kB
# License: Apache-2.0 License
# Created by: Patrick Lumbantobing, VertoX-AI
# Copyright (c) 2026 VertoX-AI. All rights reserved.
#
# This work is licensed under the Apache-2.0 License.
# To view a copy of this license, visit
# https://www.apache.org/licenses/LICENSE-2.0
"""Manage safe per-run temporary workspaces and optional diagnostic retention."""
from __future__ import annotations
import shutil
import tempfile
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from speech_text_data_aligners.core.errors import ConfigurationError
class ArtifactPolicy(StrEnum):
"""Select when a completed temporary workspace should be retained."""
NEVER = "never"
ON_FAILURE = "on_failure"
ALWAYS = "always"
@dataclass(slots=True)
class Workspace:
"""Expose a scoped temporary path and its eventual retained copy.
Parameters
----------
path: Unique temporary work directory.
retained_path: Final copied artifact directory, populated during cleanup.
Invariants:
``path`` exists only within the surrounding :func:`workspace` context.
"""
path: Path
retained_path: Path | None = None
failed: bool = False
def mark_failed(self) -> None:
"""Mark a handled failure so ``ON_FAILURE`` policy retains artifacts.
Side Effects:
Changes this workspace's cleanup disposition.
"""
self.failed = True
@contextmanager
def workspace(
*,
run_id: str,
policy: ArtifactPolicy = ArtifactPolicy.NEVER,
temporary_root: Path | None = None,
retention_root: Path | None = None,
) -> Iterator[Workspace]:
"""Create a unique temporary workspace and safely apply retention policy.
Parameters
----------
run_id: Path-component-free identifier used for a retained directory name.
policy: Retain never, only after failure, or always.
temporary_root: Optional parent for temporary workspace creation.
retention_root: Required destination parent when retention can occur.
Yields
------
Mutable workspace status object valid for the context lifetime.
Raises
------
ConfigurationError: If identifiers/roots are unsafe or retention would
overwrite an existing artifact directory.
Side Effects:
Creates a temporary directory, optionally copies it to ``retention_root``, and
always removes the temporary directory at context exit.
"""
if not run_id or Path(run_id).name != run_id or run_id in {".", ".."}:
raise ConfigurationError("workspace run_id must be one safe path component")
if policy is not ArtifactPolicy.NEVER and retention_root is None:
raise ConfigurationError("retention_root is required by artifact policy")
if temporary_root is not None:
temporary_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(
prefix=f"speech-aligners-{run_id}-",
dir=temporary_root,
) as temporary_name:
state = Workspace(Path(temporary_name))
raised = False
try:
yield state
except BaseException:
raised = True
state.failed = True
raise
finally:
should_retain = policy is ArtifactPolicy.ALWAYS or (
policy is ArtifactPolicy.ON_FAILURE and (raised or state.failed)
)
if should_retain:
assert retention_root is not None
retention_root.mkdir(parents=True, exist_ok=True)
destination = retention_root / run_id
if destination.exists():
raise ConfigurationError(
f"retained artifact destination already exists: {destination}"
)
shutil.copytree(state.path, destination)
state.retained_path = destination