storage.py 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. import datetime as dt
  2. import json
  3. import os
  4. import tempfile
  5. def utc_now_iso():
  6. """Return current UTC time in ISO 8601 format (e.g. 2026-10-13T12:00:00Z)."""
  7. return dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
  8. def read_json_file(path, default=None):
  9. """Read and parse JSON from file, returning `default` if file missing or corrupt."""
  10. if default is None:
  11. default = {}
  12. if not os.path.exists(path):
  13. return default
  14. try:
  15. with open(path, "r", encoding="utf-8") as f:
  16. return json.load(f)
  17. except (ValueError, json.JSONDecodeError):
  18. return default
  19. def _atomic_write(path, payload_bytes):
  20. """Write `payload_bytes` to `path` atomically.
  21. Strategy: write to a sibling temp file, fsync, then os.replace.
  22. Concurrent readers either see the previous version or the new
  23. one — never a truncated/empty file. Required for state.json,
  24. which is the sole fallback source on the next run.
  25. """
  26. parent = os.path.dirname(path) or "."
  27. os.makedirs(parent, exist_ok=True)
  28. fd, tmp_path = tempfile.mkstemp(
  29. prefix=".{}.".format(os.path.basename(path)),
  30. suffix=".tmp",
  31. dir=parent,
  32. )
  33. try:
  34. with os.fdopen(fd, "wb") as f:
  35. f.write(payload_bytes)
  36. f.flush()
  37. try:
  38. os.fsync(f.fileno())
  39. except OSError:
  40. # fsync may fail on some filesystems (e.g. tmpfs without backing store)
  41. pass
  42. os.replace(tmp_path, path)
  43. except Exception:
  44. try:
  45. os.unlink(tmp_path)
  46. except OSError:
  47. pass
  48. raise
  49. def write_json_file(path, data):
  50. """Atomically write data as formatted JSON."""
  51. payload = json.dumps(data, ensure_ascii=True, indent=2)
  52. _atomic_write(path, payload.encode("utf-8"))
  53. def write_text_file(path, data):
  54. """Atomically write text string to file."""
  55. _atomic_write(path, data.encode("utf-8"))