jsonable.py 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. # -*- coding: utf-8 -*-
  2. r"""响应归一化:把算法函数的返回(numpy / pandas / NaN / 时间 / Path …)压成**确定性**的 JSON 结构。
  3. 为什么要确定性:P10 的验收是"HTTP 取回的内容 == 进程内直调的内容"(逐值对拍),所以两边必须走**同一条**
  4. 归一化路径,且字典键排序、数值类型固定(NaN/Inf → null,numpy 标量 → python 标量)。
  5. """
  6. from __future__ import annotations
  7. import dataclasses
  8. import datetime as _dt
  9. import json
  10. import math
  11. import pathlib
  12. from typing import Any
  13. def to_jsonable(x: Any) -> Any:
  14. """递归归一化;不可识别的对象退回 `str()`(并保留类型名,便于排查)。"""
  15. if x is None or isinstance(x, (bool, int, str)):
  16. return x
  17. if isinstance(x, float):
  18. return None if (math.isnan(x) or math.isinf(x)) else x
  19. if isinstance(x, (_dt.datetime, _dt.date, _dt.time)):
  20. return x.isoformat()
  21. if isinstance(x, pathlib.Path):
  22. return str(x)
  23. if isinstance(x, dict):
  24. return {str(k): to_jsonable(v) for k, v in sorted(x.items(), key=lambda kv: str(kv[0]))}
  25. if isinstance(x, (list, tuple, set, frozenset)):
  26. seq = list(x)
  27. if isinstance(x, (set, frozenset)):
  28. seq = sorted(seq, key=lambda v: str(v))
  29. return [to_jsonable(v) for v in seq]
  30. if dataclasses.is_dataclass(x) and not isinstance(x, type):
  31. return to_jsonable(dataclasses.asdict(x))
  32. # numpy / pandas:不硬依赖,按鸭子类型处理
  33. for attr, kind in (('to_dict', 'dict'), ('tolist', 'list')):
  34. if hasattr(x, attr):
  35. try:
  36. got = getattr(x, attr)() if attr != 'to_dict' else x.to_dict(orient='records')
  37. return to_jsonable(got)
  38. except Exception: # noqa: BLE001 落到 str()
  39. pass
  40. if hasattr(x, 'item'): # numpy 标量
  41. try:
  42. return to_jsonable(x.item())
  43. except Exception: # noqa: BLE001
  44. pass
  45. if hasattr(x, 'shape') and hasattr(x, 'columns'): # DataFrame 兜底
  46. try:
  47. return {'columns': [str(c) for c in x.columns], 'rows': to_jsonable(x.values.tolist()),
  48. 'shape': list(x.shape)}
  49. except Exception: # noqa: BLE001
  50. pass
  51. return {'__type__': type(x).__name__, 'repr': str(x)[:500]}
  52. def dumps(payload: Any) -> str:
  53. """确定性 JSON 文本(键排序、非 ASCII 原样、紧凑分隔)—— 对拍与缓存键都用它。"""
  54. return json.dumps(to_jsonable(payload), ensure_ascii=False, sort_keys=True, separators=(',', ':'))
  55. __all__ = ['to_jsonable', 'dumps']