This commit is contained in:
2026-01-12 12:17:50 -08:00
parent 9981424397
commit b7b58f0e42
7 changed files with 21 additions and 254 deletions

View File

@@ -11,6 +11,9 @@ from SYS.logger import log
class ZeroXZero(Provider):
"""File provider for 0x0.st."""
NAME = "0x0"
PROVIDER_ALIASES = ("zeroxzero",)
def upload(self, file_path: str, **kwargs: Any) -> str:
from API.HTTP import HTTPClient
from SYS.models import ProgressFileReader

View File

@@ -142,7 +142,12 @@ class Provider(ABC):
def __init__(self, config: Optional[Dict[str, Any]] = None):
self.config = config or {}
self.name = self.__class__.__name__.lower()
# Prioritize explicit NAME property for the instance name
self.name = str(
getattr(self, "NAME", None)
or getattr(self, "PROVIDER_NAME", None)
or self.__class__.__name__
).lower()
@classmethod
def config(cls) -> List[Dict[str, Any]]:

View File

@@ -69,10 +69,10 @@ class ProviderRegistry:
if override_name:
_add(override_name)
else:
# Use class name as the primary canonical name
_add(getattr(provider_class, "__name__", None))
_add(getattr(provider_class, "PROVIDER_NAME", None))
# Use explicit NAME or PROVIDER_NAME if available, else class name
_add(getattr(provider_class, "NAME", None))
_add(getattr(provider_class, "PROVIDER_NAME", None))
_add(getattr(provider_class, "__name__", None))
for alias in getattr(provider_class, "PROVIDER_ALIASES", ()) or ():
_add(alias)

View File

@@ -27,8 +27,8 @@ CMDLET = Cmdlet(
],
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 my_store",
"Emits pipeline-friendly dicts enriched with `_selection_args` so you can use @N syntax to select and chain.",
"Example: provider-table -provider example -sample | @1 | add-file -store my_store",
],
)

View File

@@ -1,226 +0,0 @@
from __future__ import annotations
import sys
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
first_src = inputs[0].get("source") if isinstance(inputs[0], dict) else None
if not first_src:
log("Input items must include 'source' to resolve provider for selection.", file=sys.stderr)
return 1
try:
provider = get_provider(first_src)
except Exception:
log(f"Unknown provider: {first_src}", file=sys.stderr)
return 1
# Model-ize items
rows = [_dict_to_result_model(item if isinstance(item, dict) else item) for item in inputs]
# Columns: provider must supply them (no legacy defaults)
cols = provider.get_columns(rows)
# 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:
selected = raw.to_dict()
except Exception:
selected = {"title": getattr(raw, "title", str(raw))}
# Ensure selection args exist using provider's selector only
if not selected.get("_selection_args"):
try:
sel_args = provider.selection_args(rows[idx])
selected["_selection_args"] = sel_args
except Exception:
log("Selection args missing and provider selector failed.", file=sys.stderr)
return 1
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()

View File

@@ -5,11 +5,11 @@ and cmdlets to interact via a simple, pipable API.
Key ideas
- `provider-table` renders a provider result set and *emits* pipeline-friendly dicts for each row. Each emitted item includes `_selection_args`, a list of args the provider suggests for selecting that row (e.g., `['-path', '/tmp/file']`).
- `select` accepts piped items, displays a table (Rich-based), and supports selecting rows either via `-select` or `-interactive` prompt. Selected items are emitted for downstream cmdlets or you can use `-run-cmd` to invoke another cmdlet for each selected item.
- Use the `@N` syntax to select an item from a table and chain it to the next cmdlet.
Example (non-interactive):
Example:
provider-table -provider example -sample | select -select 1 | add-file -store default
provider-table -provider example -sample | @1 | add-file -store default
What providers must implement
- An adapter that yields `ResultModel` objects (breaking API).
@@ -17,6 +17,6 @@ What providers must implement
Implementation notes
- `provider-table` emits dicts like `{ 'title': ..., 'path': ..., 'metadata': ..., '_selection_args': [...] }`.
- `select` will prefer `_selection_args` if present; otherwise it will fall back to provider selection logic or sensible defaults (`-path` or `-title`).
- Selection syntax (`@1`) will prefer `_selection_args` if present; otherwise it will fall back to provider selection logic or sensible defaults (`-path` or `-title`).
This design keeps the selector-focused UX small and predictable while enabling full cmdlet interoperability via piping and `-run-cmd`.

View File

@@ -41,30 +41,15 @@ Medios-Macina is a CLI file media manager and toolkit focused on downloading, ta
<br>
<details>
<summary>console command</summary>
<summary>installation console command</summary>
<pre><code>git clone https://code.glowers.club/goyimnose/Medios-Macina.git
python Medios-Macina/scripts/bootstrap.py
</code></pre>
</details>
<br>
<b>Start the CLI by simply running "mm"</b>
</div>
- When run interactively (a normal terminal), `bootstrap.py` will show a short menu to Install or Uninstall the project.
1. rename config.conf.remove to config.conf, [config tutorial](https://code.glowers.club/goyimnose/Medios-Macina/wiki/Config.conf)
### MINIMAL EXAMPLE CONFIG - CHANGE VALUES
```Minimal config
temp="C:\\Users\\Admin\\Downloads"
[store=folder]
name="default"
path="C:\Users\Public\Documents\library"
```
3. Start the CLI by simply running "mm" in shell or run python cli.py
# [CLICK FOR GUIDED TUTORIAL](/docs/tutorial.md)