ssd
This commit is contained in:
110
SYS/json_table.py
Normal file
110
SYS/json_table.py
Normal 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
|
||||
@@ -783,56 +783,56 @@ class ResultTable:
|
||||
|
||||
def _add_search_result(self, row: ResultRow, result: Any) -> None:
|
||||
"""Extract and add SearchResult fields to row."""
|
||||
# If provider supplied explicit columns, render those and skip legacy defaults
|
||||
cols = getattr(result, "columns", None)
|
||||
used_explicit_columns = False
|
||||
if cols:
|
||||
used_explicit_columns = True
|
||||
for name, value in cols:
|
||||
row.add_column(name, value)
|
||||
return
|
||||
else:
|
||||
# Core fields (legacy fallback)
|
||||
title = getattr(result, "title", "")
|
||||
table = str(getattr(result, "table", "") or "").lower()
|
||||
|
||||
# Core fields (legacy fallback)
|
||||
title = getattr(result, "title", "")
|
||||
table = str(getattr(result, "table", "") or "").lower()
|
||||
# Handle extension separation for local files
|
||||
extension = ""
|
||||
if title and table == "local":
|
||||
path_obj = Path(title)
|
||||
if path_obj.suffix:
|
||||
extension = path_obj.suffix.lstrip(".")
|
||||
title = path_obj.stem
|
||||
|
||||
# Handle extension separation for local files
|
||||
extension = ""
|
||||
if title and table == "local":
|
||||
path_obj = Path(title)
|
||||
if path_obj.suffix:
|
||||
extension = path_obj.suffix.lstrip(".")
|
||||
title = path_obj.stem
|
||||
if title:
|
||||
row.add_column("Title", title)
|
||||
|
||||
if title:
|
||||
row.add_column("Title", title)
|
||||
# Extension column
|
||||
row.add_column("Ext", extension)
|
||||
|
||||
# Extension column
|
||||
row.add_column("Ext", extension)
|
||||
if hasattr(result, "table") and getattr(result, "table", None):
|
||||
row.add_column("Source", str(getattr(result, "table")))
|
||||
|
||||
if hasattr(result, "table") and getattr(result, "table", None):
|
||||
row.add_column("Source", str(getattr(result, "table")))
|
||||
if hasattr(result, "detail") and result.detail:
|
||||
row.add_column("Detail", result.detail)
|
||||
|
||||
if hasattr(result, "detail") and result.detail:
|
||||
row.add_column("Detail", result.detail)
|
||||
if hasattr(result, "media_kind") and result.media_kind:
|
||||
row.add_column("Type", result.media_kind)
|
||||
|
||||
if hasattr(result, "media_kind") and result.media_kind:
|
||||
row.add_column("Type", result.media_kind)
|
||||
# Tag summary
|
||||
if hasattr(result, "tag_summary") and result.tag_summary:
|
||||
row.add_column("Tag", str(result.tag_summary))
|
||||
|
||||
# Tag summary
|
||||
if hasattr(result, "tag_summary") and result.tag_summary:
|
||||
row.add_column("Tag", str(result.tag_summary))
|
||||
# Duration (for media)
|
||||
if hasattr(result, "duration_seconds") and result.duration_seconds:
|
||||
dur = _format_duration_hms(result.duration_seconds)
|
||||
row.add_column("Duration", dur or str(result.duration_seconds))
|
||||
|
||||
# Duration (for media)
|
||||
if hasattr(result, "duration_seconds") and result.duration_seconds:
|
||||
dur = _format_duration_hms(result.duration_seconds)
|
||||
row.add_column("Duration", dur or str(result.duration_seconds))
|
||||
# Size (for files)
|
||||
if hasattr(result, "size_bytes") and result.size_bytes:
|
||||
row.add_column("Size", _format_size(result.size_bytes, integer_only=False))
|
||||
|
||||
# Size (for files)
|
||||
if hasattr(result, "size_bytes") and result.size_bytes:
|
||||
row.add_column("Size", _format_size(result.size_bytes, integer_only=False))
|
||||
|
||||
# Annotations
|
||||
if hasattr(result, "annotations") and result.annotations:
|
||||
row.add_column("Annotations", ", ".join(str(a) for a in result.annotations))
|
||||
# Annotations
|
||||
if hasattr(result, "annotations") and result.annotations:
|
||||
row.add_column("Annotations", ", ".join(str(a) for a in result.annotations))
|
||||
|
||||
try:
|
||||
md = getattr(result, "full_metadata", None)
|
||||
|
||||
@@ -61,15 +61,22 @@ class Provider:
|
||||
|
||||
def serialize_row(self, row: ResultModel) -> Dict[str, Any]:
|
||||
r = ensure_result_model(row)
|
||||
return {
|
||||
metadata = r.metadata or {}
|
||||
out: Dict[str, Any] = {
|
||||
"title": r.title,
|
||||
"path": r.path,
|
||||
"ext": r.ext,
|
||||
"size_bytes": r.size_bytes,
|
||||
"metadata": r.metadata or {},
|
||||
"metadata": metadata,
|
||||
"source": r.source or self.name,
|
||||
"_selection_args": self.selection_args(r),
|
||||
}
|
||||
selection_action = metadata.get("_selection_action") or metadata.get("selection_action")
|
||||
if selection_action:
|
||||
out["_selection_action"] = [
|
||||
str(x) for x in selection_action if x is not None
|
||||
]
|
||||
return out
|
||||
|
||||
def serialize_rows(self, rows: Iterable[ResultModel]) -> List[Dict[str, Any]]:
|
||||
return [self.serialize_row(r) for r in rows]
|
||||
|
||||
Reference in New Issue
Block a user