0bc734c020
Applies `ruff format` to the accumulated formatting debt on this branch. Formatting-only — no behavioral changes. Required for CI/lint's format gate (`nox -s format -- --check`), which the branch was failing on 288 tracked files that drifted from ruff's canonical style. In-progress WIP files are intentionally excluded so this commit stays a clean formatting-only diff. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
72 lines
2.6 KiB
Python
72 lines
2.6 KiB
Python
"""Safe JSON encoding helper.
|
|
|
|
Phase 1k++ refinement (N5/R4): we used to pass ``default=str`` to
|
|
``json.dumps`` so nested datetime/Decimal fields didn't crash the
|
|
write AFTER the agent had done its real work. But ``default=str`` is
|
|
too permissive — a worker that accidentally emits a ``set`` or a
|
|
custom class gets silently stringified to ``"<MyObj at 0x...>"``,
|
|
masking a real output bug.
|
|
|
|
This module ships a restricted encoder that:
|
|
- Encodes ``datetime`` and ``date`` as ISO-8601 strings.
|
|
- Encodes ``Decimal`` as its string repr (preserves precision).
|
|
- Encodes ``UUID`` as its canonical string form.
|
|
- Encodes ``Path`` as its string form.
|
|
- Raises ``TypeError`` for anything else — including ``set`` /
|
|
``frozenset`` (round 3 R4: silent set→list coercion contradicted
|
|
the "raise loudly" design intent because JSON has no native set
|
|
and a reader doing ``parsed["tags"]`` would get a list, losing
|
|
set algebra. Callers that genuinely want a JSON array should call
|
|
``sorted(list(my_set))`` explicitly).
|
|
|
|
Scope (round-3 R8): this encoder is used at the boundaries where
|
|
WORKER-ORIGINATED payloads are persisted — i.e., the runner's
|
|
output-write and the scheduler's input_payload write. Other
|
|
``json.dumps`` call sites in the controller (event-row payloads,
|
|
discovery markers, etc.) serialize fixed-shape dicts of native types
|
|
and don't need restriction.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from uuid import UUID
|
|
|
|
# Public list of types this encoder coerces. Anything else → TypeError.
|
|
_SAFE_TYPES = (datetime, date, Decimal, UUID, Path)
|
|
|
|
|
|
def _restricted_default(value: Any) -> Any:
|
|
if isinstance(value, datetime):
|
|
return value.isoformat()
|
|
if isinstance(value, date):
|
|
return value.isoformat()
|
|
if isinstance(value, Decimal):
|
|
return str(value)
|
|
if isinstance(value, UUID):
|
|
return str(value)
|
|
if isinstance(value, Path):
|
|
return str(value)
|
|
raise TypeError(
|
|
f"Object of type {type(value).__name__} is not JSON-serializable "
|
|
f"and not in the controller's safe-type allowlist "
|
|
f"({', '.join(t.__name__ for t in _SAFE_TYPES)}). "
|
|
f"To emit a set as a JSON array, call sorted(list(...)) "
|
|
f"explicitly at the producer."
|
|
)
|
|
|
|
|
|
def safe_json_dumps(obj: Any, **kwargs: Any) -> str:
|
|
"""json.dumps with a restricted ``default`` that handles the
|
|
allowlist + raises for anything else.
|
|
|
|
Extra kwargs are forwarded to ``json.dumps``."""
|
|
return json.dumps(obj, default=_restricted_default, **kwargs)
|
|
|
|
|
|
__all__ = ["safe_json_dumps"]
|