Source code for ezmsg.dashboard.backend.json_encoding

"""Wire encoding for values JSON cannot represent natively.

JSON has no literal for ``inf``, ``-inf`` or ``nan``, and the two transports the
dashboard uses disagree about what to do with them: ``JSONResponse`` renders
with ``allow_nan=False`` and raises, while pydantic's ``model_dump(mode="json")``
silently rewrites them to ``null``. Settings classes are free to hold such
values (``clip_max: float = np.inf`` is perfectly reasonable), so the dashboard
encodes them as the string tokens below on the way out and decodes them again on
the way in.

The tokens are spelled the way JavaScript spells them, so the frontend can
recover the value with ``Number(token)``.
"""

from __future__ import annotations

import math
from typing import Any

INFINITY_TOKEN = "Infinity"
NEGATIVE_INFINITY_TOKEN = "-Infinity"
NAN_TOKEN = "NaN"

NON_FINITE_TOKENS = (INFINITY_TOKEN, NEGATIVE_INFINITY_TOKEN, NAN_TOKEN)


[docs] def encode_float(value: float) -> float | str: """Return ``value`` unchanged, or its token if it is not finite.""" if math.isfinite(value): return value if math.isnan(value): return NAN_TOKEN return INFINITY_TOKEN if value > 0 else NEGATIVE_INFINITY_TOKEN
[docs] def decode_float_token(value: Any) -> float | None: """Return the float a token stands for, or ``None`` if it is not a token.""" if not isinstance(value, str): return None if value == INFINITY_TOKEN: return math.inf if value == NEGATIVE_INFINITY_TOKEN: return -math.inf if value == NAN_TOKEN: return math.nan return None
[docs] def is_non_finite_token(value: Any) -> bool: return isinstance(value, str) and value in NON_FINITE_TOKENS
[docs] def sanitize_json_value(value: Any) -> Any: """Recursively encode non-finite floats anywhere in an already-JSON-safe payload. Applied to whole responses as a backstop; payloads built by :mod:`~ezmsg.dashboard.backend.services.adapters` are already encoded. """ if isinstance(value, float): return encode_float(value) if isinstance(value, dict): return {key: sanitize_json_value(item) for key, item in value.items()} if isinstance(value, (list, tuple)): return [sanitize_json_value(item) for item in value] return value