This commit is contained in:
2026-01-06 16:19:29 -08:00
parent 41c11d39fd
commit edc33f4528
10 changed files with 1192 additions and 881 deletions

110
SYS/json_table.py Normal file
View File

@@ -0,0 +1,110 @@
"""Helper utilities for normalizing JSON result tables.
This mirrors the intent of the existing `SYS.html_table` helper but operates on
JSON payloads (API responses, JSON APIs, etc.). It exposes:
- `extract_records` for locating and normalizing the first list of record dicts
from a JSON document.
- `normalize_record` for coercing arbitrary values into printable strings.
These helpers make it easy for providers that consume JSON to populate
`ResultModel` metadata without hand-writing ad-hoc sanitizers.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Sequence, Tuple
_DEFAULT_LIST_KEYS: Tuple[str, ...] = ("results", "items", "docs", "records")
def _coerce_value(value: Any) -> str:
"""Convert a JSON value into a compact string representation."""
if value is None:
return ""
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (list, tuple, set)):
parts = [_coerce_value(v) for v in value]
cleaned = [part for part in parts if part]
return ", ".join(cleaned)
if isinstance(value, dict):
parts: List[str] = []
for subkey, subvalue in value.items():
part = _coerce_value(subvalue)
if part:
parts.append(f"{subkey}:{part}")
return ", ".join(parts)
try:
return str(value).strip()
except Exception:
return ""
def normalize_record(record: Dict[str, Any]) -> Dict[str, str]:
"""Return a copy of ``record`` with keys lowered and values coerced to strings."""
out: Dict[str, str] = {}
if not isinstance(record, dict):
return out
for key, value in record.items():
normalized_key = str(key or "").strip().lower()
if not normalized_key:
continue
normalized_value = _coerce_value(value)
if normalized_value:
out[normalized_key] = normalized_value
return out
def _traverse(data: Any, path: Sequence[str]) -> Optional[Any]:
current = data
for key in path:
if not isinstance(current, dict):
return None
current = current.get(key)
return current
def extract_records(
data: Any,
*,
path: Optional[Sequence[str]] = None,
list_keys: Optional[Sequence[str]] = None,
) -> Tuple[List[Dict[str, str]], Optional[str]]:
"""Extract normalized record dicts from ``data``.
Args:
data: JSON document (dict/list) that may contain tabular records.
path: optional key path to traverse before looking for a list.
list_keys: candidate keys to inspect when ``path`` is not provided.
Returns:
(records, chosen_path) where ``records`` is the list of normalized dicts
and ``chosen_path`` is either the traversed path or the key that matched.
"""
list_keys = list_keys or _DEFAULT_LIST_KEYS
chosen_path: Optional[str] = None
candidates: List[Any] = []
if path:
found = _traverse(data, path)
if isinstance(found, list):
candidates = found
chosen_path = ".".join(path)
if not candidates and isinstance(data, dict):
for key in list_keys:
found = data.get(key)
if isinstance(found, list):
candidates = found
chosen_path = key
break
if not candidates and isinstance(data, list):
candidates = data
chosen_path = ""
records: List[Dict[str, str]] = []
for entry in candidates:
if isinstance(entry, dict):
records.append(normalize_record(entry))
return records, chosen_path