HTTP: prefer pip-system-certs/certifi_win32 bundle; use init-time verify in retries; add tests
This commit is contained in:
80
SYS/result_table_adapters.py
Normal file
80
SYS/result_table_adapters.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""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
|
||||
|
||||
from SYS.result_table_api import ColumnSpec, ProviderAdapter, ResultModel
|
||||
|
||||
|
||||
ColumnFactory = Callable[[Iterable[ResultModel]], List[ColumnSpec]]
|
||||
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
|
||||
columns: Optional[Union[List[ColumnSpec], ColumnFactory]] = None
|
||||
selection_fn: Optional[SelectionFn] = None
|
||||
metadata: Optional[Dict[str, Any]] = None
|
||||
|
||||
def get_columns(self, rows: Optional[Iterable[ResultModel]] = None) -> List[ColumnSpec]:
|
||||
if callable(self.columns):
|
||||
try:
|
||||
rows_list = list(rows) if rows is not None else []
|
||||
return list(self.columns(rows_list))
|
||||
except Exception:
|
||||
# Fall back to a minimal Title column on errors
|
||||
return [ColumnSpec("title", "Title", lambda r: r.title)]
|
||||
if self.columns is not None:
|
||||
return list(self.columns)
|
||||
# Default minimal column set
|
||||
return [ColumnSpec("title", "Title", lambda r: r.title)]
|
||||
|
||||
def selection_args(self, row: ResultModel) -> List[str]:
|
||||
if callable(self.selection_fn):
|
||||
try:
|
||||
return list(self.selection_fn(row))
|
||||
except Exception:
|
||||
return []
|
||||
# Default selector: prefer path flag, then title
|
||||
if getattr(row, "path", None):
|
||||
return ["-path", str(row.path)]
|
||||
return ["-title", str(row.title)]
|
||||
|
||||
|
||||
_PROVIDERS: Dict[str, Provider] = {}
|
||||
|
||||
|
||||
def register_provider(
|
||||
name: str,
|
||||
adapter: ProviderAdapter,
|
||||
*,
|
||||
columns: Optional[Union[List[ColumnSpec], ColumnFactory]] = None,
|
||||
selection_fn: Optional[SelectionFn] = None,
|
||||
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}")
|
||||
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:
|
||||
return _PROVIDERS[name.lower()]
|
||||
|
||||
|
||||
def list_providers() -> List[str]:
|
||||
return list(_PROVIDERS.keys())
|
||||
109
SYS/result_table_api.py
Normal file
109
SYS/result_table_api.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""ResultTable API types and small helpers (breaking: no legacy compatibility).
|
||||
|
||||
This module provides the canonical dataclasses and protocols that providers and
|
||||
renderers must use. It intentionally refuses to accept legacy dicts/strings/objs
|
||||
— adapters must produce `ResultModel` instances.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Callable, Dict, Iterable, Optional, Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ResultModel:
|
||||
"""Canonical result model that providers must produce.
|
||||
|
||||
Fields:
|
||||
- title: human-friendly title (required)
|
||||
- path: optional filesystem path/URL
|
||||
- ext: file extension (without dot), e.g. "pdf", "mp3"
|
||||
- size_bytes: optional size in bytes
|
||||
- media_kind: one of 'video','audio','image','doc', etc.
|
||||
- metadata: arbitrary provider-specific metadata
|
||||
- source: provider name string
|
||||
"""
|
||||
|
||||
title: str
|
||||
path: Optional[str] = None
|
||||
ext: Optional[str] = None
|
||||
size_bytes: Optional[int] = None
|
||||
media_kind: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
source: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ColumnSpec:
|
||||
"""Specification for a column that renderers will use.
|
||||
|
||||
extractor: callable that accepts a ResultModel and returns the cell value.
|
||||
format_fn: optional callable used to format the extracted value to string.
|
||||
"""
|
||||
|
||||
name: str
|
||||
header: str
|
||||
extractor: Callable[[ResultModel], Any]
|
||||
format_fn: Optional[Callable[[Any], str]] = None
|
||||
width: Optional[int] = None
|
||||
sortable: bool = False
|
||||
|
||||
|
||||
ProviderAdapter = Callable[[Iterable[Any]], Iterable[ResultModel]]
|
||||
"""Type for provider adapters.
|
||||
|
||||
Adapters must accept provider-specific sequence/iterable and yield
|
||||
`ResultModel` instances only. Anything else is an error (no implicit normalization).
|
||||
"""
|
||||
|
||||
|
||||
class Renderer(Protocol):
|
||||
"""Renderer protocol.
|
||||
|
||||
Implementations should accept rows and columns and return a renderable
|
||||
or side-effect (e.g., print to terminal). Keep implementations deterministic
|
||||
and side-effect-free when possible (return a Rich renderable object).
|
||||
"""
|
||||
|
||||
def render(self, rows: Iterable[ResultModel], columns: Iterable[ColumnSpec], meta: Optional[Dict[str, Any]] = None) -> Any: # pragma: no cover - interface
|
||||
...
|
||||
|
||||
|
||||
# Small helper enforcing strict API usage
|
||||
|
||||
def ensure_result_model(obj: Any) -> ResultModel:
|
||||
"""Ensure `obj` is a `ResultModel` instance, else raise TypeError.
|
||||
|
||||
This makes the API intentionally strict: providers must construct ResultModel
|
||||
objects.
|
||||
"""
|
||||
if isinstance(obj, ResultModel):
|
||||
return obj
|
||||
raise TypeError("ResultModel required; providers must yield ResultModel instances")
|
||||
|
||||
|
||||
# Convenience column spec generators
|
||||
|
||||
def title_column() -> ColumnSpec:
|
||||
return ColumnSpec("title", "Title", lambda r: r.title)
|
||||
|
||||
|
||||
def ext_column() -> ColumnSpec:
|
||||
return ColumnSpec("ext", "Ext", lambda r: r.ext or "")
|
||||
|
||||
|
||||
# Helper to build a ColumnSpec that extracts a metadata key from ResultModel
|
||||
def metadata_column(key: str, header: Optional[str] = None, format_fn: Optional[Callable[[Any], str]] = None) -> ColumnSpec:
|
||||
hdr = header or str(key).replace("_", " ").title()
|
||||
return ColumnSpec(name=key, header=hdr, extractor=lambda r: (r.metadata or {}).get(key), format_fn=format_fn)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ResultModel",
|
||||
"ColumnSpec",
|
||||
"ProviderAdapter",
|
||||
"Renderer",
|
||||
"ensure_result_model",
|
||||
"title_column",
|
||||
"ext_column",
|
||||
]
|
||||
67
SYS/result_table_renderers.py
Normal file
67
SYS/result_table_renderers.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Renderers for the ResultTable API.
|
||||
|
||||
This module provides a Rich-based Renderer implementation that returns a
|
||||
`rich.table.Table` renderable. The implementation is intentionally small and
|
||||
focused on the command-line display use-case; keep logic side-effect-free where
|
||||
possible and let callers decide whether to `Console.print()` or capture output.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
from SYS.result_table_api import ColumnSpec, ResultModel, Renderer
|
||||
|
||||
|
||||
class RichRenderer(Renderer):
|
||||
"""Rich renderer implementing the `Renderer` protocol.
|
||||
|
||||
Usage:
|
||||
from rich.console import Console
|
||||
table = RichRenderer().render(rows, columns, meta)
|
||||
Console().print(table)
|
||||
"""
|
||||
|
||||
def render(self, rows: Iterable[ResultModel], columns: Iterable[ColumnSpec], meta: Optional[Dict[str, Any]] = None) -> Any: # pragma: no cover - simple wrapper
|
||||
try:
|
||||
from rich.table import Table as RichTable
|
||||
except Exception as exc:
|
||||
raise RuntimeError("rich is required for RichRenderer") from exc
|
||||
|
||||
table = RichTable(show_header=True, header_style="bold")
|
||||
cols = list(columns)
|
||||
for col in cols:
|
||||
table.add_column(col.header)
|
||||
|
||||
for r in rows:
|
||||
cells = []
|
||||
for col in cols:
|
||||
try:
|
||||
raw = col.extractor(r)
|
||||
if col.format_fn:
|
||||
try:
|
||||
cell = col.format_fn(raw)
|
||||
except Exception:
|
||||
cell = str(raw or "")
|
||||
else:
|
||||
cell = str(raw or "")
|
||||
except Exception:
|
||||
cell = ""
|
||||
cells.append(cell)
|
||||
table.add_row(*cells)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
# Small convenience function
|
||||
|
||||
def render_to_console(rows: Iterable[ResultModel], columns: Iterable[ColumnSpec], meta: Optional[Dict[str, Any]] = None) -> None:
|
||||
try:
|
||||
from rich.console import Console
|
||||
except Exception:
|
||||
# If rich isn't present, fall back to simple text output
|
||||
for r in rows:
|
||||
print(" ".join(str((col.extractor(r) or "")) for col in columns))
|
||||
return
|
||||
|
||||
table = RichRenderer().render(rows, columns, meta)
|
||||
Console().print(table)
|
||||
Reference in New Issue
Block a user