40 lines
1.2 KiB
Python
40 lines
1.2 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import tempfile
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def utc_now_iso() -> str:
|
|
"""Return a stable UTC timestamp for manifests and capture artifacts."""
|
|
return datetime.now(UTC).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
|
|
|
|
def write_json_atomic(path: Path, payload: Any) -> None:
|
|
"""Write JSON without exposing a partially written artifact."""
|
|
path = path.expanduser()
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
serialized = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
|
|
|
temp_name: str | None = None
|
|
try:
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w",
|
|
encoding="utf-8",
|
|
dir=path.parent,
|
|
prefix=f".{path.name}.",
|
|
suffix=".tmp",
|
|
delete=False,
|
|
) as stream:
|
|
temp_name = stream.name
|
|
stream.write(serialized)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
Path(temp_name).replace(path)
|
|
finally:
|
|
if temp_name is not None:
|
|
Path(temp_name).unlink(missing_ok=True)
|