2026-01-05 13:09:24 -08:00
|
|
|
"""Provider registry for ResultTable API (breaking, strict API).
|
|
|
|
|
|
|
|
|
|
Providers register themselves here with an adapter and optional column factory
|
|
|
|
|
and selection function. Consumers (cmdlets) can look up providers by name and
|
|
|
|
|
obtain the columns and selection behavior for building tables and for selection
|
|
|
|
|
args used by subsequent cmdlets.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from typing import Any, Callable, Dict, Iterable, List, Optional, Union
|
|
|
|
|
|
2026-01-06 01:38:59 -08:00
|
|
|
from SYS.result_table_api import ColumnSpec, ProviderAdapter, ResultModel, ResultTable, ensure_result_model
|
2026-01-05 13:09:24 -08:00
|
|
|
|
|
|
|
|
|
2026-01-06 01:38:59 -08:00
|
|
|
ColumnFactory = Callable[[List[ResultModel]], List[ColumnSpec]]
|
2026-01-05 13:09:24 -08:00
|
|
|
SelectionFn = Callable[[ResultModel], List[str]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Provider:
|
|
|
|
|
name: str
|
|
|
|
|
adapter: ProviderAdapter
|
|
|
|
|
# columns can be a static list or a factory that derives columns from sample rows
|
2026-01-06 01:38:59 -08:00
|
|
|
columns: Union[List[ColumnSpec], ColumnFactory]
|
|
|
|
|
selection_fn: SelectionFn
|
2026-01-05 13:09:24 -08:00
|
|
|
metadata: Optional[Dict[str, Any]] = None
|
|
|
|
|
|
|
|
|
|
def get_columns(self, rows: Optional[Iterable[ResultModel]] = None) -> List[ColumnSpec]:
|
2026-01-06 01:38:59 -08:00
|
|
|
if self.columns is None:
|
|
|
|
|
raise ValueError(f"provider '{self.name}' must define columns")
|
|
|
|
|
|
2026-01-05 13:09:24 -08:00
|
|
|
if callable(self.columns):
|
2026-01-06 01:38:59 -08:00
|
|
|
rows_list = list(rows) if rows is not None else []
|
|
|
|
|
cols = list(self.columns(rows_list))
|
|
|
|
|
else:
|
|
|
|
|
cols = list(self.columns)
|
|
|
|
|
|
|
|
|
|
if not cols:
|
|
|
|
|
raise ValueError(f"provider '{self.name}' produced no columns")
|
|
|
|
|
|
|
|
|
|
return cols
|
2026-01-05 13:09:24 -08:00
|
|
|
|
|
|
|
|
def selection_args(self, row: ResultModel) -> List[str]:
|
2026-01-06 01:38:59 -08:00
|
|
|
if not callable(self.selection_fn):
|
|
|
|
|
raise ValueError(f"provider '{self.name}' must define a selection function")
|
|
|
|
|
|
|
|
|
|
sel = list(self.selection_fn(ensure_result_model(row)))
|
|
|
|
|
return sel
|
|
|
|
|
|
|
|
|
|
def build_table(self, items: Iterable[Any]) -> ResultTable:
|
|
|
|
|
"""Materialize adapter output into a ResultTable (strict, no legacy types)."""
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
rows = [ensure_result_model(r) for r in self.adapter(items)]
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
raise RuntimeError(f"provider '{self.name}' adapter failed") from exc
|
|
|
|
|
|
|
|
|
|
cols = self.get_columns(rows)
|
|
|
|
|
return ResultTable(provider=self.name, rows=rows, columns=cols, meta=self.metadata or {})
|
|
|
|
|
|
|
|
|
|
def serialize_row(self, row: ResultModel) -> Dict[str, Any]:
|
|
|
|
|
r = ensure_result_model(row)
|
2026-01-06 16:19:29 -08:00
|
|
|
metadata = r.metadata or {}
|
|
|
|
|
out: Dict[str, Any] = {
|
2026-01-06 01:38:59 -08:00
|
|
|
"title": r.title,
|
|
|
|
|
"path": r.path,
|
|
|
|
|
"ext": r.ext,
|
|
|
|
|
"size_bytes": r.size_bytes,
|
2026-01-06 16:19:29 -08:00
|
|
|
"metadata": metadata,
|
2026-01-06 01:38:59 -08:00
|
|
|
"source": r.source or self.name,
|
|
|
|
|
"_selection_args": self.selection_args(r),
|
|
|
|
|
}
|
2026-01-06 16:19:29 -08:00
|
|
|
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
|
2026-01-06 01:38:59 -08:00
|
|
|
|
|
|
|
|
def serialize_rows(self, rows: Iterable[ResultModel]) -> List[Dict[str, Any]]:
|
|
|
|
|
return [self.serialize_row(r) for r in rows]
|
2026-01-05 13:09:24 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
_PROVIDERS: Dict[str, Provider] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def register_provider(
|
|
|
|
|
name: str,
|
|
|
|
|
adapter: ProviderAdapter,
|
|
|
|
|
*,
|
2026-01-06 01:38:59 -08:00
|
|
|
columns: Union[List[ColumnSpec], ColumnFactory],
|
|
|
|
|
selection_fn: SelectionFn,
|
2026-01-05 13:09:24 -08:00
|
|
|
metadata: Optional[Dict[str, Any]] = None,
|
|
|
|
|
) -> Provider:
|
|
|
|
|
name = str(name or "").strip().lower()
|
|
|
|
|
if not name:
|
|
|
|
|
raise ValueError("provider name required")
|
|
|
|
|
if name in _PROVIDERS:
|
|
|
|
|
raise ValueError(f"provider already registered: {name}")
|
2026-01-06 01:38:59 -08:00
|
|
|
if columns is None:
|
|
|
|
|
raise ValueError("provider registration requires columns")
|
|
|
|
|
if selection_fn is None:
|
|
|
|
|
raise ValueError("provider registration requires selection_fn")
|
2026-01-05 13:09:24 -08:00
|
|
|
p = Provider(name=name, adapter=adapter, columns=columns, selection_fn=selection_fn, metadata=metadata)
|
|
|
|
|
_PROVIDERS[name] = p
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_provider(name: str) -> Provider:
|
2026-01-06 01:38:59 -08:00
|
|
|
normalized = str(name or "").lower()
|
|
|
|
|
if normalized not in _PROVIDERS:
|
|
|
|
|
raise KeyError(f"provider not registered: {name}")
|
|
|
|
|
return _PROVIDERS[normalized]
|
2026-01-05 13:09:24 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_providers() -> List[str]:
|
|
|
|
|
return list(_PROVIDERS.keys())
|