HTTP: prefer pip-system-certs/certifi_win32 bundle; use init-time verify in retries; add tests
This commit is contained in:
@@ -128,12 +128,24 @@ def _doc_convert(input_path: Path, output_path: Path) -> bool:
|
||||
return False
|
||||
|
||||
target_fmt = output_path.suffix.lstrip(".").lower() or "pdf"
|
||||
extra_args = []
|
||||
|
||||
if target_fmt == "pdf":
|
||||
tectonic_path = shutil.which("tectonic")
|
||||
if not tectonic_path:
|
||||
log(
|
||||
"tectonic is required for PDF output; install with `pip install tectonic`",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return False
|
||||
extra_args = ["--pdf-engine=tectonic"]
|
||||
|
||||
try:
|
||||
pypandoc.convert_file(
|
||||
str(input_path),
|
||||
to=target_fmt,
|
||||
outputfile=str(output_path),
|
||||
extra_args=extra_args,
|
||||
)
|
||||
except OSError as exc:
|
||||
log(f"pandoc is missing or failed to run: {exc}", file=sys.stderr)
|
||||
@@ -163,7 +175,7 @@ CMDLET = Cmdlet(
|
||||
detail=[
|
||||
"Allows video↔video, audio↔audio, image↔image, doc↔doc, and video→audio conversions.",
|
||||
"Disallows incompatible conversions (e.g., video→pdf).",
|
||||
"Uses ffmpeg for media and pypandoc-binary (bundled pandoc) for document formats (mobi/epub→pdf/txt/etc).",
|
||||
"Uses ffmpeg for media and pypandoc-binary (bundled pandoc) for document formats (mobi/epub→pdf/txt/etc); PDF output uses the tectonic LaTeX engine when available.",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
157
cmdlet/provider_table.py
Normal file
157
cmdlet/provider_table.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Iterable, Optional, Sequence
|
||||
from pathlib import Path
|
||||
|
||||
from . import _shared as sh
|
||||
from SYS.logger import log, debug
|
||||
from SYS import pipeline as ctx
|
||||
|
||||
from SYS.result_table_adapters import get_provider
|
||||
from SYS.result_table_renderers import RichRenderer
|
||||
|
||||
Cmdlet = sh.Cmdlet
|
||||
CmdletArg = sh.CmdletArg
|
||||
parse_cmdlet_args = sh.parse_cmdlet_args
|
||||
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name="provider-table",
|
||||
summary="Render a provider's result set and optionally run a follow-up cmdlet using the selected row.",
|
||||
usage="provider-table -provider <name> [-sample] [-select <n>] [-run-cmd <name>]",
|
||||
arg=[
|
||||
CmdletArg("provider", type="string", description="Provider name to render (default: example)"),
|
||||
CmdletArg("sample", type="flag", description="Use provider sample/demo items when available."),
|
||||
CmdletArg("select", type="int", description="1-based row index to select and use for follow-up command."),
|
||||
CmdletArg("run-cmd", type="string", description="Cmdlet to invoke with the selected row's selector args."),
|
||||
],
|
||||
detail=[
|
||||
"Use a registered provider to build a table and optionally run another cmdlet with selection args.",
|
||||
"Emits pipeline-friendly dicts enriched with `_selection_args` so you can pipe into `select` and other cmdlets.",
|
||||
"Example: provider-table -provider example -sample | select -select 1 | add-file -store default",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
parsed = parse_cmdlet_args(args, CMDLET)
|
||||
|
||||
provider_name = parsed.get("provider") or "example"
|
||||
use_sample = bool(parsed.get("sample", False))
|
||||
run_cmd = parsed.get("run-cmd")
|
||||
select_raw = parsed.get("select")
|
||||
|
||||
try:
|
||||
provider = get_provider(provider_name)
|
||||
except Exception as exc:
|
||||
log(f"Unknown provider: {provider_name}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Obtain items to feed to the adapter
|
||||
items = None
|
||||
if use_sample:
|
||||
# Try to locate SAMPLE_ITEMS in the adapter's module (convention only)
|
||||
try:
|
||||
mod = __import__(provider.adapter.__module__, fromlist=["*"])
|
||||
items = getattr(mod, "SAMPLE_ITEMS", None)
|
||||
if items is None:
|
||||
log("Provider does not expose SAMPLE_ITEMS; no sample available", file=sys.stderr)
|
||||
return 1
|
||||
except Exception:
|
||||
log("Failed to load provider sample", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
# Require input for non-sample runs
|
||||
inputs = list(result) if isinstance(result, Iterable) else []
|
||||
if not inputs:
|
||||
log("No input provided. Use -sample for demo or pipe provider items in.", file=sys.stderr)
|
||||
return 1
|
||||
items = inputs
|
||||
|
||||
# Build rows
|
||||
try:
|
||||
rows = list(provider.adapter(items))
|
||||
except Exception as exc:
|
||||
log(f"Provider adapter failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
cols = provider.get_columns(rows)
|
||||
|
||||
# Emit rows for downstream pipeline consumption (pipable behavior).
|
||||
try:
|
||||
for r in rows:
|
||||
try:
|
||||
item = {
|
||||
"title": getattr(r, "title", None) or None,
|
||||
"path": getattr(r, "path", None) or None,
|
||||
"ext": getattr(r, "ext", None) or None,
|
||||
"size_bytes": getattr(r, "size_bytes", None) or None,
|
||||
"metadata": getattr(r, "metadata", None) or {},
|
||||
"source": getattr(r, "source", None) or provider.name,
|
||||
"_selection_args": provider.selection_args(r),
|
||||
}
|
||||
ctx.emit(item)
|
||||
except Exception:
|
||||
# Best-effort: continue emitting other rows
|
||||
continue
|
||||
except Exception:
|
||||
# Non-fatal: continue to rendering even if emission fails
|
||||
pass
|
||||
|
||||
# Render using RichRenderer
|
||||
try:
|
||||
table = RichRenderer().render(rows, cols, provider.metadata)
|
||||
try:
|
||||
from rich.console import Console
|
||||
|
||||
Console().print(table)
|
||||
except Exception:
|
||||
# Fallback to simple printing
|
||||
for r in rows:
|
||||
print(" ".join(str((c.extractor(r) or "")) for c in cols))
|
||||
except Exception as exc:
|
||||
log(f"Rendering failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# If no selection requested, we're done
|
||||
if not select_raw:
|
||||
return 0
|
||||
|
||||
try:
|
||||
select_idx = int(select_raw) - 1
|
||||
except Exception:
|
||||
log("Invalid -select value; must be an integer", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if select_idx < 0 or select_idx >= len(rows):
|
||||
log("-select out of range", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
selected = rows[select_idx]
|
||||
sel_args = provider.selection_args(selected)
|
||||
|
||||
if not run_cmd:
|
||||
# Print selection args for caller
|
||||
log(f"Selection args: {sel_args}", file=sys.stderr)
|
||||
return 0
|
||||
|
||||
# Run follow-up cmdlet
|
||||
try:
|
||||
from cmdlet import ensure_cmdlet_modules_loaded, get as get_cmdlet
|
||||
|
||||
ensure_cmdlet_modules_loaded()
|
||||
cmd_fn = get_cmdlet(run_cmd)
|
||||
if not cmd_fn:
|
||||
log(f"Follow-up cmdlet not found: {run_cmd}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Call the cmdlet with no upstream result, but with selection args
|
||||
ret = cmd_fn(None, sel_args, config or {})
|
||||
return ret
|
||||
except Exception as exc:
|
||||
log(f"Failed to invoke follow-up cmdlet: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
CMDLET.exec = _run
|
||||
CMDLET.register()
|
||||
238
cmdlet/select_item.py
Normal file
238
cmdlet/select_item.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Sequence
|
||||
from . import _shared as sh
|
||||
from SYS.logger import log, debug
|
||||
from SYS import pipeline as ctx
|
||||
|
||||
from SYS.result_table_api import ResultModel
|
||||
from SYS.result_table_adapters import get_provider
|
||||
from SYS.result_table_renderers import RichRenderer
|
||||
|
||||
Cmdlet = sh.Cmdlet
|
||||
CmdletArg = sh.CmdletArg
|
||||
parse_cmdlet_args = sh.parse_cmdlet_args
|
||||
normalize_result_input = sh.normalize_result_input
|
||||
|
||||
|
||||
CMDLET = Cmdlet(
|
||||
name="select",
|
||||
summary="Select items from a piped result set (interactive or via -select) and emit the selected item(s).",
|
||||
usage="select -select <n|1-3|1,3> [-multi] [-run-cmd <name>]",
|
||||
arg=[
|
||||
CmdletArg("select", type="string", description="Selection string (e.g., 1, 2-4)", alias="s"),
|
||||
CmdletArg("multi", type="flag", description="Allow multiple selections."),
|
||||
CmdletArg("interactive", type="flag", description="Prompt interactively for selection."),
|
||||
CmdletArg("run-cmd", type="string", description="Cmdlet to invoke with selected items (each)"),
|
||||
],
|
||||
detail=[
|
||||
"Accepts piped input from provider-table or other sources and emits the selected item(s) as dicts.",
|
||||
"If -run-cmd is provided, invokes the named cmdlet for each selected item with selector args and the item as piped input.",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _parse_selection(selection: str, max_len: int) -> List[int]:
|
||||
"""Parse a selection string like '1', '1-3', '1,3,5-7' into 0-based indices."""
|
||||
if not selection:
|
||||
return []
|
||||
parts = [p.strip() for p in str(selection).split(",") if p.strip()]
|
||||
indices = set()
|
||||
for part in parts:
|
||||
if "-" in part:
|
||||
try:
|
||||
a, b = part.split("-", 1)
|
||||
start = int(a.strip())
|
||||
end = int(b.strip())
|
||||
if start > end:
|
||||
start, end = end, start
|
||||
for i in range(start, end + 1):
|
||||
if 1 <= i <= max_len:
|
||||
indices.add(i - 1)
|
||||
except Exception:
|
||||
raise ValueError(f"Invalid range: {part}")
|
||||
else:
|
||||
try:
|
||||
v = int(part)
|
||||
if 1 <= v <= max_len:
|
||||
indices.add(v - 1)
|
||||
except Exception:
|
||||
raise ValueError(f"Invalid selection: {part}")
|
||||
return sorted(indices)
|
||||
|
||||
|
||||
def _dict_to_result_model(d: Dict[str, Any]) -> ResultModel:
|
||||
if isinstance(d, ResultModel):
|
||||
return d
|
||||
# Allow dicts or objects with attributes
|
||||
title = d.get("title") or d.get("name") or (d.get("path") and str(d.get("path")).split("/")[-1])
|
||||
return ResultModel(
|
||||
title=str(title) if title is not None else "",
|
||||
path=d.get("path") if d.get("path") is not None else None,
|
||||
ext=d.get("ext") if d.get("ext") is not None else None,
|
||||
size_bytes=d.get("size_bytes") if d.get("size_bytes") is not None else None,
|
||||
metadata=d.get("metadata") or {},
|
||||
source=d.get("source") or None,
|
||||
)
|
||||
|
||||
|
||||
def _run(result: Any, args: Sequence[str], config: Dict[str, Any]) -> int:
|
||||
parsed = parse_cmdlet_args(args, CMDLET)
|
||||
|
||||
select_raw = parsed.get("select")
|
||||
allow_multi = bool(parsed.get("multi", False))
|
||||
interactive = bool(parsed.get("interactive", False))
|
||||
run_cmd = parsed.get("run-cmd")
|
||||
|
||||
inputs = normalize_result_input(result)
|
||||
if not inputs:
|
||||
log("No input provided to select; pipe provider-table output or use a cmdlet that emits items.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Model-ize items
|
||||
rows = [_dict_to_result_model(item if isinstance(item, dict) else item) for item in inputs]
|
||||
|
||||
# Attempt to detect provider from first item
|
||||
provider = None
|
||||
first_src = inputs[0].get("source") if isinstance(inputs[0], dict) else None
|
||||
if first_src:
|
||||
try:
|
||||
provider = get_provider(first_src)
|
||||
except Exception:
|
||||
provider = None
|
||||
|
||||
# Columns: ask provider for column spec if available, else build minimal columns
|
||||
if provider:
|
||||
cols = provider.get_columns(rows)
|
||||
else:
|
||||
# Minimal columns built from available keys
|
||||
from SYS.result_table_api import title_column, ext_column
|
||||
|
||||
cols = [title_column()]
|
||||
if any(r.ext for r in rows):
|
||||
cols.append(ext_column())
|
||||
|
||||
# Render table to console
|
||||
try:
|
||||
table = RichRenderer().render(rows, cols, None)
|
||||
try:
|
||||
from rich.console import Console
|
||||
|
||||
Console().print(table)
|
||||
except Exception:
|
||||
for r in rows:
|
||||
print(" ".join(str((c.extractor(r) or "")) for c in cols))
|
||||
except Exception as exc:
|
||||
log(f"Rendering failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Determine selection indices
|
||||
indices: List[int] = []
|
||||
if select_raw:
|
||||
try:
|
||||
indices = _parse_selection(str(select_raw), len(rows))
|
||||
except ValueError as exc:
|
||||
log(str(exc), file=sys.stderr)
|
||||
return 1
|
||||
elif interactive:
|
||||
# Prompt user (single index only unless multi)
|
||||
try:
|
||||
from rich.prompt import Prompt
|
||||
|
||||
prompt_text = "Select item(s) (e.g., 1 or 1,3-5)"
|
||||
if not allow_multi:
|
||||
prompt_text += " (single)"
|
||||
choice = Prompt.ask(prompt_text).strip()
|
||||
indices = _parse_selection(choice, len(rows))
|
||||
except Exception as exc:
|
||||
log(f"Interactive selection failed: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
else:
|
||||
log("No selection requested. Use -select or -interactive.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if not indices:
|
||||
log("No valid selection indices provided", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Build selected items and emit
|
||||
selected_items: List[Dict[str, Any]] = []
|
||||
for idx in indices:
|
||||
try:
|
||||
raw = inputs[idx] if idx < len(inputs) else None
|
||||
if isinstance(raw, dict):
|
||||
selected = dict(raw)
|
||||
elif isinstance(raw, ResultModel):
|
||||
selected = {
|
||||
"title": raw.title,
|
||||
"path": raw.path,
|
||||
"ext": raw.ext,
|
||||
"size_bytes": raw.size_bytes,
|
||||
"metadata": raw.metadata or {},
|
||||
"source": raw.source,
|
||||
}
|
||||
else:
|
||||
# try to call to_dict or fallback
|
||||
try:
|
||||
selected = raw.to_dict()
|
||||
except Exception:
|
||||
selected = {"title": getattr(raw, "title", str(raw))}
|
||||
|
||||
# Ensure selection args exist
|
||||
if not selected.get("_selection_args"):
|
||||
if provider:
|
||||
try:
|
||||
sel_args = provider.selection_args(rows[idx])
|
||||
selected["_selection_args"] = sel_args
|
||||
except Exception:
|
||||
selected["_selection_args"] = []
|
||||
else:
|
||||
# fallback
|
||||
if selected.get("path"):
|
||||
selected["_selection_args"] = ["-path", selected.get("path")]
|
||||
else:
|
||||
selected["_selection_args"] = ["-title", selected.get("title") or ""]
|
||||
|
||||
selected_items.append(selected)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Emit selected items so downstream cmdlets can consume them
|
||||
try:
|
||||
for itm in selected_items:
|
||||
ctx.emit(itm)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Optionally run follow-up cmdlet for each selected item
|
||||
if run_cmd:
|
||||
try:
|
||||
from cmdlet import ensure_cmdlet_modules_loaded, get as get_cmdlet
|
||||
|
||||
ensure_cmdlet_modules_loaded()
|
||||
cmd_fn = get_cmdlet(run_cmd)
|
||||
if not cmd_fn:
|
||||
log(f"Follow-up cmdlet not found: {run_cmd}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
exit_code = 0
|
||||
for itm in selected_items:
|
||||
sel_args = itm.get("_selection_args") or []
|
||||
# Invoke follow-up cmdlet with the selected item as piped input
|
||||
try:
|
||||
ret = cmd_fn(itm, sel_args, config or {})
|
||||
except Exception as exc:
|
||||
log(f"Follow-up cmdlet raised: {exc}", file=sys.stderr)
|
||||
ret = 1
|
||||
if ret != 0:
|
||||
exit_code = ret
|
||||
return exit_code
|
||||
except Exception as exc:
|
||||
log(f"Failed to invoke follow-up cmdlet: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
CMDLET.exec = _run
|
||||
CMDLET.register()
|
||||
Reference in New Issue
Block a user