HTTP: prefer pip-system-certs/certifi_win32 bundle; use init-time verify in retries; add tests
This commit is contained in:
259
Provider/example_provider.py
Normal file
259
Provider/example_provider.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""Example provider that uses the new `ResultTable` API.
|
||||
|
||||
This module demonstrates a minimal provider adapter that yields `ResultModel`
|
||||
instances, a set of `ColumnSpec` definitions, and a tiny CLI-friendly renderer
|
||||
(`render_table`) for demonstration.
|
||||
|
||||
Run this to see sample output:
|
||||
python -m Provider.example_provider
|
||||
|
||||
Example usage (piped selector):
|
||||
provider-table -provider example -sample | select -select 1 | add-file -store default
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List
|
||||
|
||||
from SYS.result_table_api import ColumnSpec, ResultModel, title_column, ext_column
|
||||
|
||||
|
||||
SAMPLE_ITEMS = [
|
||||
{
|
||||
"name": "Book of Awe.pdf",
|
||||
"path": "sample/Book of Awe.pdf",
|
||||
"ext": "pdf",
|
||||
"size": 1024000,
|
||||
"source": "example",
|
||||
},
|
||||
{
|
||||
"name": "Song of Joy.mp3",
|
||||
"path": "sample/Song of Joy.mp3",
|
||||
"ext": "mp3",
|
||||
"size": 5120000,
|
||||
"source": "example",
|
||||
},
|
||||
{
|
||||
"name": "Cover Image.jpg",
|
||||
"path": "sample/Cover Image.jpg",
|
||||
"ext": "jpg",
|
||||
"size": 20480,
|
||||
"source": "example",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def adapter(items: Iterable[Dict[str, Any]]) -> Iterable[ResultModel]:
|
||||
"""Convert provider-specific items into `ResultModel` instances.
|
||||
|
||||
This adapter enforces the strict API requirement: it yields only
|
||||
`ResultModel` instances (no legacy dict objects).
|
||||
"""
|
||||
for it in items:
|
||||
title = it.get("name") or it.get("title") or (Path(str(it.get("path"))).stem if it.get("path") else "")
|
||||
yield ResultModel(
|
||||
title=str(title),
|
||||
path=str(it.get("path")) if it.get("path") else None,
|
||||
ext=str(it.get("ext")) if it.get("ext") else None,
|
||||
size_bytes=int(it.get("size")) if it.get("size") is not None else None,
|
||||
metadata=dict(it),
|
||||
source=str(it.get("source")) if it.get("source") else "example",
|
||||
)
|
||||
|
||||
|
||||
# Columns are intentionally *not* mandated. Create a factory that inspects
|
||||
# sample rows and builds only columns that make sense for the provider data.
|
||||
from SYS.result_table_api import metadata_column
|
||||
|
||||
|
||||
def columns_factory(rows: List[ResultModel]) -> List[ColumnSpec]:
|
||||
cols: List[ColumnSpec] = [title_column()]
|
||||
|
||||
# If any row has an extension, include Ext column
|
||||
if any(getattr(r, "ext", None) for r in rows):
|
||||
cols.append(ext_column())
|
||||
|
||||
# If any row has size, include Size column
|
||||
if any(getattr(r, "size_bytes", None) for r in rows):
|
||||
cols.append(ColumnSpec("size", "Size", lambda rr: rr.size_bytes or "", lambda v: _format_size(v)))
|
||||
|
||||
# Add any top-level metadata keys discovered (up to 3) as optional columns
|
||||
seen_keys = []
|
||||
for r in rows:
|
||||
for k in (r.metadata or {}).keys():
|
||||
if k in ("name", "title", "path"):
|
||||
continue
|
||||
if k not in seen_keys:
|
||||
seen_keys.append(k)
|
||||
if len(seen_keys) >= 3:
|
||||
break
|
||||
if len(seen_keys) >= 3:
|
||||
break
|
||||
|
||||
for k in seen_keys:
|
||||
cols.append(metadata_column(k))
|
||||
|
||||
return cols
|
||||
|
||||
|
||||
# Selection function: cmdlets rely on this to build selector args when the user
|
||||
# selects a row (e.g., '@3' -> run next-cmd with the returned args). Prefer
|
||||
# -path if available, otherwise fall back to -title.
|
||||
def selection_fn(row: ResultModel) -> List[str]:
|
||||
if row.path:
|
||||
return ["-path", row.path]
|
||||
return ["-title", row.title]
|
||||
|
||||
|
||||
# Register the provider with the registry so callers can discover it by name
|
||||
from SYS.result_table_adapters import register_provider
|
||||
register_provider(
|
||||
"example",
|
||||
adapter,
|
||||
columns=columns_factory,
|
||||
selection_fn=selection_fn,
|
||||
metadata={"description": "Example provider demonstrating dynamic columns and selectors"},
|
||||
)
|
||||
|
||||
|
||||
def _format_size(size: Any) -> str:
|
||||
try:
|
||||
s = int(size)
|
||||
except Exception:
|
||||
return ""
|
||||
if s >= 1024 ** 3:
|
||||
return f"{s / (1024 ** 3):.2f} GB"
|
||||
if s >= 1024 ** 2:
|
||||
return f"{s / (1024 ** 2):.2f} MB"
|
||||
if s >= 1024:
|
||||
return f"{s / 1024:.2f} KB"
|
||||
return f"{s} B"
|
||||
|
||||
|
||||
def render_table(rows: Iterable[ResultModel], columns: List[ColumnSpec]) -> str:
|
||||
"""Render a simple ASCII table of `rows` using `columns`.
|
||||
|
||||
This is intentionally very small and dependency-free for demonstration.
|
||||
Renderers in the project should implement the `Renderer` protocol.
|
||||
"""
|
||||
rows = list(rows)
|
||||
|
||||
# Build cell matrix (strings)
|
||||
matrix: List[List[str]] = []
|
||||
for r in rows:
|
||||
cells: List[str] = []
|
||||
for col in columns:
|
||||
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 "")
|
||||
cells.append(cell)
|
||||
matrix.append(cells)
|
||||
|
||||
# Compute column widths as max(header, content)
|
||||
headers = [c.header for c in columns]
|
||||
widths = [len(h) for h in headers]
|
||||
for row_cells in matrix:
|
||||
for i, cell in enumerate(row_cells):
|
||||
widths[i] = max(widths[i], len(cell))
|
||||
|
||||
# Helper to format a row
|
||||
def fmt_row(cells: List[str]) -> str:
|
||||
return " | ".join(cell.ljust(widths[i]) for i, cell in enumerate(cells))
|
||||
|
||||
lines: List[str] = []
|
||||
lines.append(fmt_row(headers))
|
||||
lines.append("-+-".join("-" * w for w in widths))
|
||||
for row_cells in matrix:
|
||||
lines.append(fmt_row(row_cells))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# Rich-based renderer (returns a Rich Table renderable)
|
||||
def render_table_rich(rows: Iterable[ResultModel], columns: List[ColumnSpec]):
|
||||
"""Render rows as a `rich.table.Table` for terminal output.
|
||||
|
||||
Returns the Table object; callers may `Console.print(table)` to render.
|
||||
"""
|
||||
try:
|
||||
from rich.table import Table as RichTable
|
||||
except Exception as exc: # pragma: no cover - rare if rich missing
|
||||
raise RuntimeError("rich is required for rich renderer") from exc
|
||||
|
||||
table = RichTable(show_header=True, header_style="bold")
|
||||
for col in columns:
|
||||
table.add_column(col.header)
|
||||
|
||||
for r in rows:
|
||||
cells: List[str] = []
|
||||
for col in columns:
|
||||
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 "")
|
||||
cells.append(cell)
|
||||
table.add_row(*cells)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def demo() -> None:
|
||||
rows = list(adapter(SAMPLE_ITEMS))
|
||||
table = render_table_rich(rows, columns_factory(rows))
|
||||
try:
|
||||
from rich.console import Console
|
||||
except Exception:
|
||||
# Fall back to plain printing if rich is not available
|
||||
print("Example provider output:")
|
||||
print(render_table(rows, columns_factory(rows)))
|
||||
return
|
||||
|
||||
console = Console()
|
||||
console.print("Example provider output:")
|
||||
console.print(table)
|
||||
|
||||
|
||||
def demo_with_selection(idx: int = 0) -> None:
|
||||
"""Demonstrate how a cmdlet would use provider registration and selection args.
|
||||
|
||||
- Fetch the registered provider by name
|
||||
- Build rows via adapter
|
||||
- Render the table
|
||||
- Show the selection args for the chosen row; these are the args a cmdlet
|
||||
would append when the user picks that row.
|
||||
"""
|
||||
from SYS.result_table_adapters import get_provider
|
||||
|
||||
provider = get_provider("example")
|
||||
rows = list(provider.adapter(SAMPLE_ITEMS))
|
||||
cols = provider.get_columns(rows)
|
||||
|
||||
# Render
|
||||
try:
|
||||
from rich.console import Console
|
||||
except Exception:
|
||||
print(render_table(rows, cols))
|
||||
sel_args = provider.selection_args(rows[idx])
|
||||
print("Selection args for row", idx, "->", sel_args)
|
||||
return
|
||||
|
||||
console = Console()
|
||||
console.print("Example provider output:")
|
||||
console.print(render_table_rich(rows, cols))
|
||||
|
||||
# Selection args example
|
||||
sel = provider.selection_args(rows[idx])
|
||||
console.print("Selection args for row", idx, "->", sel)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
108
Provider/vimm.py
108
Provider/vimm.py
@@ -5,7 +5,7 @@ starting point for implementing a full Vimm (vimm.net) provider.
|
||||
|
||||
It prefers server-rendered HTML parsing via lxml and uses the repo's
|
||||
`HTTPClient` helper for robust HTTP calls (timeouts/retries).
|
||||
|
||||
|
||||
Selectors in `search()` are intentionally permissive heuristics; update the
|
||||
XPaths to match the real site HTML when you have an actual fixture.
|
||||
"""
|
||||
@@ -78,6 +78,11 @@ class Vimm(Provider):
|
||||
resp = client.get(url)
|
||||
content = resp.content
|
||||
except Exception as exc:
|
||||
# Log and return empty results on failure. The HTTP client will
|
||||
# already attempt a certifi-based retry in common certificate
|
||||
# verification failure cases; if you still see cert errors, install
|
||||
# the `certifi` package or configure SSL_CERT_FILE to point at a
|
||||
# valid CA bundle.
|
||||
log(f"[vimm] HTTP fetch failed: {exc}", file=sys.stderr)
|
||||
return []
|
||||
|
||||
@@ -183,3 +188,104 @@ class Vimm(Provider):
|
||||
continue
|
||||
|
||||
return results[: max(0, int(limit))]
|
||||
|
||||
|
||||
# Bridge into the ResultTable provider registry so vimm results can be rendered
|
||||
# with the new provider/table/select API.
|
||||
try:
|
||||
from SYS.result_table_adapters import register_provider
|
||||
from SYS.result_table_api import ResultModel
|
||||
from SYS.result_table_api import title_column, ext_column, metadata_column
|
||||
|
||||
def _convert_search_result_to_model(sr):
|
||||
try:
|
||||
if hasattr(sr, "to_dict"):
|
||||
d = sr.to_dict()
|
||||
elif isinstance(sr, dict):
|
||||
d = sr
|
||||
else:
|
||||
d = {
|
||||
"title": getattr(sr, "title", str(sr)),
|
||||
"path": getattr(sr, "path", None),
|
||||
"size_bytes": getattr(sr, "size_bytes", None),
|
||||
"columns": getattr(sr, "columns", None),
|
||||
"full_metadata": getattr(sr, "full_metadata", None),
|
||||
}
|
||||
except Exception:
|
||||
d = {"title": getattr(sr, "title", str(sr))}
|
||||
|
||||
title = d.get("title") or ""
|
||||
path = d.get("path") or None
|
||||
size = d.get("size_bytes") or None
|
||||
ext = None
|
||||
try:
|
||||
if path:
|
||||
from pathlib import Path
|
||||
|
||||
suf = Path(str(path)).suffix
|
||||
if suf:
|
||||
ext = suf.lstrip(".")
|
||||
except Exception:
|
||||
ext = None
|
||||
|
||||
metadata = d.get("full_metadata") or d.get("metadata") or {}
|
||||
return ResultModel(
|
||||
title=str(title),
|
||||
path=str(path) if path is not None else None,
|
||||
ext=str(ext) if ext is not None else None,
|
||||
size_bytes=int(size) if size is not None else None,
|
||||
metadata=metadata or {},
|
||||
source="vimm",
|
||||
)
|
||||
|
||||
def _adapter(items):
|
||||
for it in items:
|
||||
yield _convert_search_result_to_model(it)
|
||||
|
||||
def _columns_factory(rows):
|
||||
cols = [title_column()]
|
||||
if any(getattr(r, "ext", None) for r in rows):
|
||||
cols.append(ext_column())
|
||||
if any(getattr(r, "size_bytes", None) for r in rows):
|
||||
cols.append(metadata_column("size", "Size"))
|
||||
# Add up to 2 discovered metadata keys from rows
|
||||
seen = []
|
||||
for r in rows:
|
||||
for k in (r.metadata or {}).keys():
|
||||
if k in ("name", "title", "path"):
|
||||
continue
|
||||
if k not in seen:
|
||||
seen.append(k)
|
||||
if len(seen) >= 2:
|
||||
break
|
||||
if len(seen) >= 2:
|
||||
break
|
||||
for k in seen:
|
||||
cols.append(metadata_column(k))
|
||||
return cols
|
||||
|
||||
def _selection_fn(row):
|
||||
if getattr(row, "path", None):
|
||||
return ["-path", row.path]
|
||||
return ["-title", row.title or ""]
|
||||
|
||||
SAMPLE_ITEMS = [
|
||||
{"title": "Room of Awe", "path": "sample/Room of Awe", "ext": "zip", "size_bytes": 1024 * 1024 * 12, "full_metadata": {"platform": "PC"}},
|
||||
{"title": "Song of Joy", "path": "sample/Song of Joy.mp3", "ext": "mp3", "size_bytes": 5120000, "full_metadata": {"platform": "PC"}},
|
||||
{"title": "Cover Image", "path": "sample/Cover.jpg", "ext": "jpg", "size_bytes": 20480, "full_metadata": {}},
|
||||
]
|
||||
|
||||
try:
|
||||
register_provider(
|
||||
"vimm",
|
||||
_adapter,
|
||||
columns=_columns_factory,
|
||||
selection_fn=_selection_fn,
|
||||
metadata={"description": "Vimm provider bridge (ProviderCore -> ResultTable API)"},
|
||||
)
|
||||
except Exception:
|
||||
# Non-fatal: registration is best-effort
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user