bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
114 lines
3.9 KiB
Python
114 lines
3.9 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
import io
|
|
import json
|
|
import tarfile
|
|
|
|
from mirage.workspace.snapshot.manifest import resolve_manifest
|
|
from mirage.workspace.snapshot.utils import is_safe_blob_path
|
|
|
|
_MANIFEST_NAME = "manifest.json"
|
|
|
|
_COMPRESS_MODES = {None: "w", "gz": "w:gz", "bz2": "w:bz2", "xz": "w:xz"}
|
|
|
|
|
|
def write_tar(target,
|
|
manifest: dict,
|
|
blobs: dict[str, bytes],
|
|
*,
|
|
compress: str | None = None) -> None:
|
|
"""Write manifest + blobs as a tar.
|
|
|
|
Args:
|
|
target: filesystem path (str/Path) OR a writable file-like
|
|
object with a `write` method (BytesIO, etc.).
|
|
manifest: JSON-serializable dict.
|
|
blobs: {tar_path: bytes} side-files.
|
|
compress: None | "gz" | "bz2" | "xz".
|
|
"""
|
|
if compress not in _COMPRESS_MODES:
|
|
raise ValueError(
|
|
f"Unknown compress mode: {compress!r}. "
|
|
f"Use one of: {sorted(k for k in _COMPRESS_MODES if k)}")
|
|
mode = _COMPRESS_MODES[compress]
|
|
if hasattr(target, "write"):
|
|
tar = tarfile.open(fileobj=target, mode=mode)
|
|
else:
|
|
tar = tarfile.open(str(target), mode)
|
|
with tar:
|
|
manifest_bytes = json.dumps(manifest, indent=2,
|
|
default=_json_default).encode("utf-8")
|
|
_add(tar, _MANIFEST_NAME, manifest_bytes)
|
|
for tar_path, data in blobs.items():
|
|
_add(tar, tar_path, data)
|
|
|
|
|
|
def read_tar(source) -> dict:
|
|
"""Read a tar produced by write_tar; return a resolved state dict.
|
|
|
|
Args:
|
|
source: filesystem path (str/Path) OR a readable file-like
|
|
object with a `read` method.
|
|
"""
|
|
if hasattr(source, "read"):
|
|
tar = tarfile.open(fileobj=source, mode="r:*")
|
|
else:
|
|
tar = tarfile.open(str(source), "r:*")
|
|
with tar:
|
|
member = tar.getmember(_MANIFEST_NAME)
|
|
f = tar.extractfile(member)
|
|
if f is None:
|
|
raise ValueError(f"{_MANIFEST_NAME} missing or unreadable")
|
|
manifest = json.loads(f.read().decode("utf-8"))
|
|
return resolve_manifest(manifest, _make_reader(tar))
|
|
|
|
|
|
def _make_reader(tar):
|
|
|
|
def reader(blob_path: str) -> bytes:
|
|
if not is_safe_blob_path(blob_path):
|
|
raise ValueError(f"Unsafe blob path: {blob_path!r}")
|
|
try:
|
|
member = tar.getmember(blob_path)
|
|
except KeyError as exc:
|
|
raise ValueError(
|
|
f"Manifest references missing blob: {blob_path!r}") from exc
|
|
f = tar.extractfile(member)
|
|
if f is None:
|
|
raise ValueError(f"Blob unreadable: {blob_path!r}")
|
|
return f.read()
|
|
|
|
return reader
|
|
|
|
|
|
def _add(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
|
info = tarfile.TarInfo(name=name)
|
|
info.size = len(data)
|
|
info.mode = 0o644
|
|
tar.addfile(info, io.BytesIO(data))
|
|
|
|
|
|
def _json_default(obj):
|
|
# StrEnum members serialize as their string value (already happens
|
|
# since StrEnum inherits from str), but bytes leftover in the
|
|
# manifest are a programmer error — they should have been split
|
|
# to blob refs earlier.
|
|
if isinstance(obj, bytes):
|
|
raise TypeError(
|
|
"Bytes leftover in manifest — split_manifest_and_blobs "
|
|
"must replace every bytes value with a blob ref")
|
|
raise TypeError(
|
|
f"Object of type {type(obj).__name__} not JSON serializable")
|