| from __future__ import annotations |
|
|
| import json |
| import os |
| import zipfile |
| from dataclasses import dataclass |
| from datetime import datetime, timezone |
| from pathlib import Path |
| from typing import Any, Callable, Iterable |
|
|
|
|
| @dataclass(frozen=True) |
| class ProbeMember: |
| """One file included in a returned Probe bundle.""" |
|
|
| role: str |
| path: Path |
| arcname: str | None = None |
| required: bool = True |
|
|
|
|
| def _normalized_members(members: Iterable[ProbeMember]) -> list[ProbeMember]: |
| out: list[ProbeMember] = [] |
| seen_arcnames: set[str] = set() |
| for member in members: |
| path = Path(member.path) |
| if not path.exists(): |
| if member.required: |
| raise FileNotFoundError(f"Required Probe member is missing: {path}") |
| continue |
| if not path.is_file(): |
| raise ValueError(f"Probe member must be a file: {path}") |
| arcname = str(member.arcname or path.name).strip() |
| if not arcname or Path(arcname).is_absolute() or ".." in Path(arcname).parts: |
| raise ValueError(f"Unsafe Probe archive name: {arcname!r}") |
| if arcname in seen_arcnames: |
| raise ValueError(f"Duplicate Probe archive name: {arcname}") |
| seen_arcnames.add(arcname) |
| out.append(ProbeMember(role=str(member.role), path=path, arcname=arcname, required=member.required)) |
| return out |
|
|
|
|
| def build_probe_zip( |
| output_path: str | Path, |
| *, |
| candidate_id: str, |
| request_id: str, |
| status: str, |
| members: Iterable[ProbeMember], |
| sha256_file: Callable[[str | Path], str], |
| metadata: dict[str, Any] | None = None, |
| ) -> Path: |
| """Build one self-describing Probe ZIP without leaking absolute runtime paths.""" |
|
|
| output = Path(output_path) |
| output.parent.mkdir(parents=True, exist_ok=True) |
| normalized = _normalized_members(members) |
|
|
| manifest_files = [] |
| for member in normalized: |
| assert member.arcname is not None |
| manifest_files.append( |
| { |
| "role": member.role, |
| "name": member.arcname, |
| "size_bytes": int(member.path.stat().st_size), |
| "sha256": sha256_file(member.path), |
| } |
| ) |
|
|
| manifest = { |
| "schema_version": "ltx25-probe-bundle-v1", |
| "candidate_id": str(candidate_id), |
| "request_id": str(request_id), |
| "status": str(status).upper(), |
| "created_utc": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"), |
| "files": manifest_files, |
| "metadata": dict(metadata or {}), |
| } |
|
|
| tmp = output.with_name(f".{output.name}.{os.getpid()}.tmp") |
| try: |
| with zipfile.ZipFile(tmp, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as archive: |
| archive.writestr("probe_manifest.json", json.dumps(manifest, indent=2, sort_keys=True) + "\n") |
| for member in normalized: |
| assert member.arcname is not None |
| archive.write(member.path, arcname=member.arcname) |
| tmp.replace(output) |
| finally: |
| try: |
| tmp.unlink(missing_ok=True) |
| except Exception: |
| pass |
| return output |
|
|
|
|
| def build_request_probe( |
| paths: Any, |
| *, |
| candidate_id: str, |
| status: str, |
| sha256_file: Callable[[str | Path], str], |
| include_run_info: bool = True, |
| include_diagnostics: bool = True, |
| include_log: bool = True, |
| extra_members: Iterable[ProbeMember] = (), |
| metadata: dict[str, Any] | None = None, |
| ) -> Path: |
| """Bundle the ordinary request evidence files owned by one RequestPaths object.""" |
|
|
| members: list[ProbeMember] = [] |
| if include_run_info: |
| members.append(ProbeMember("run_info", Path(paths.run_info), required=False)) |
| if include_diagnostics: |
| members.append(ProbeMember("diagnostics", Path(paths.diagnostics), required=False)) |
| if include_log: |
| members.append(ProbeMember("request_log", Path(paths.log), required=False)) |
| members.extend(extra_members) |
| return build_probe_zip( |
| paths.probe, |
| candidate_id=candidate_id, |
| request_id=paths.request_id, |
| status=status, |
| members=members, |
| sha256_file=sha256_file, |
| metadata=metadata, |
| ) |
|
|
|
|
| def append_probe_path(existing: Any, new_path: str | Path, *, limit: int | None = None) -> list[str]: |
| """Append one Probe bundle path while preserving existing gr.File-style history.""" |
|
|
| values: list[str] = [] |
| if isinstance(existing, (list, tuple)): |
| values.extend(str(item) for item in existing if item) |
| elif existing: |
| values.append(str(existing)) |
|
|
| new_value = str(new_path) |
| values = [item for item in values if item != new_value] |
| values.append(new_value) |
| if limit is not None and int(limit) > 0: |
| values = values[-int(limit) :] |
| return values |
|
|
|
|
| def list_session_probe_paths(worker_root: str | Path, session_id: str, *, limit: int | None = None) -> list[str]: |
| """Return existing request Probe ZIPs for one UUID session, oldest-to-newest.""" |
|
|
| session_root = Path(worker_root) / str(session_id or "").strip().lower() |
| if not session_root.is_dir(): |
| return [] |
| found = [path for path in session_root.glob("*/ltx25_probe_*.zip") if path.is_file()] |
| found.sort(key=lambda path: (path.stat().st_mtime_ns, path.name)) |
| values = [str(path) for path in found] |
| if limit is not None and int(limit) > 0: |
| values = values[-int(limit) :] |
| return values |
|
|