Source code for stilt.storage.layout
"""Resolved project roots and path helpers."""
from __future__ import annotations
import re
import tempfile
from dataclasses import dataclass
from pathlib import Path
def resolve_directory(
directory: str | Path | None = None, *, prefix: str = "pystilt_"
) -> Path:
"""Return a resolved directory path, creating a temp root when omitted."""
if directory is None:
return Path(tempfile.mkdtemp(prefix=prefix))
directory = Path(directory)
if directory.parent == Path("."):
directory = directory.resolve()
return directory
def is_cloud_project(project: str) -> bool:
"""Return True when *project* is an object-storage URI."""
return project.startswith(("s3://", "gs://"))
def project_slug(project: str) -> str:
"""Derive a DNS-safe/local-safe slug from a project path or URI."""
raw = project.rstrip("/")
if "://" in raw:
raw = raw.split("://", 1)[1]
parts = [part for part in raw.split("/") if part]
candidate = parts[-1] if parts else "project"
slug = candidate.lower().replace("_", "-")
slug = re.sub(r"[^a-z0-9-]+", "-", slug)
slug = re.sub(r"-{2,}", "-", slug).strip("-")
return slug or "project"
def uri_join(root: str, *parts: str) -> str:
"""Join path fragments onto a local path or object-store URI."""
clean_parts = [part.strip("/") for part in parts if part and part.strip("/")]
if "://" in root:
base = root.rstrip("/")
if not clean_parts:
return base
return f"{base}/{'/'.join(clean_parts)}"
path = Path(root)
for part in clean_parts:
path /= part
return str(path)
[docs]
@dataclass(frozen=True, slots=True)
class ProjectLayout:
"""Resolved local directories and output refs for one model instance."""
project_ref: str
output_ref: str
is_cloud_project: bool
is_cloud_output: bool
project_dir: Path
output_dir: Path
[docs]
@classmethod
def resolve(
cls,
project: str | Path | None,
output_dir: str | Path | None,
) -> ProjectLayout:
"""Resolve project/output refs and any required local working roots."""
project_ref = str(project or "")
output_ref = str(output_dir) if output_dir is not None else project_ref
if not project_ref and output_dir is not None:
project_ref = output_ref
project_is_cloud = is_cloud_project(project_ref)
output_is_cloud = is_cloud_project(output_ref)
project_dir: str | Path | None = None
output_local_dir: str | Path | None = None
if not project_is_cloud:
project_dir = project or (
output_dir if output_dir is not None and not output_is_cloud else None
)
if not output_is_cloud:
output_local_dir = output_dir or project or None
return cls(
project_ref=project_ref,
output_ref=output_ref,
is_cloud_project=project_is_cloud,
is_cloud_output=output_is_cloud,
project_dir=resolve_directory(project_dir),
output_dir=resolve_directory(output_local_dir),
)
@property
def project_root(self) -> str:
"""Return the canonical project ref or resolved local directory."""
return self.project_ref if self.is_cloud_project else str(self.project_dir)
@property
def output_root(self) -> str:
"""Return the canonical output ref or resolved local directory."""
return self.output_ref if self.is_cloud_output else str(self.output_dir)