h
This commit is contained in:
@@ -11,6 +11,9 @@ from SYS.logger import log
|
|||||||
class ZeroXZero(Provider):
|
class ZeroXZero(Provider):
|
||||||
"""File provider for 0x0.st."""
|
"""File provider for 0x0.st."""
|
||||||
|
|
||||||
|
NAME = "0x0"
|
||||||
|
PROVIDER_ALIASES = ("zeroxzero",)
|
||||||
|
|
||||||
def upload(self, file_path: str, **kwargs: Any) -> str:
|
def upload(self, file_path: str, **kwargs: Any) -> str:
|
||||||
from API.HTTP import HTTPClient
|
from API.HTTP import HTTPClient
|
||||||
from SYS.models import ProgressFileReader
|
from SYS.models import ProgressFileReader
|
||||||
|
|||||||
@@ -142,7 +142,12 @@ class Provider(ABC):
|
|||||||
|
|
||||||
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
def __init__(self, config: Optional[Dict[str, Any]] = None):
|
||||||
self.config = config or {}
|
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
|
@classmethod
|
||||||
def config(cls) -> List[Dict[str, Any]]:
|
def config(cls) -> List[Dict[str, Any]]:
|
||||||
|
|||||||
@@ -69,10 +69,10 @@ class ProviderRegistry:
|
|||||||
if override_name:
|
if override_name:
|
||||||
_add(override_name)
|
_add(override_name)
|
||||||
else:
|
else:
|
||||||
# Use class name as the primary canonical name
|
# 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))
|
_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 ():
|
for alias in getattr(provider_class, "PROVIDER_ALIASES", ()) or ():
|
||||||
_add(alias)
|
_add(alias)
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ CMDLET = Cmdlet(
|
|||||||
],
|
],
|
||||||
detail=[
|
detail=[
|
||||||
"Use a registered provider to build a table and optionally run another cmdlet with selection args.",
|
"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.",
|
"Emits pipeline-friendly dicts enriched with `_selection_args` so you can use @N syntax to select and chain.",
|
||||||
"Example: provider-table -provider example -sample | select -select 1 | add-file -store my_store",
|
"Example: provider-table -provider example -sample | @1 | add-file -store my_store",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -5,11 +5,11 @@ and cmdlets to interact via a simple, pipable API.
|
|||||||
|
|
||||||
Key ideas
|
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']`).
|
- `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
|
What providers must implement
|
||||||
- An adapter that yields `ResultModel` objects (breaking API).
|
- An adapter that yields `ResultModel` objects (breaking API).
|
||||||
@@ -17,6 +17,6 @@ What providers must implement
|
|||||||
|
|
||||||
Implementation notes
|
Implementation notes
|
||||||
- `provider-table` emits dicts like `{ 'title': ..., 'path': ..., 'metadata': ..., '_selection_args': [...] }`.
|
- `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`.
|
This design keeps the selector-focused UX small and predictable while enabling full cmdlet interoperability via piping and `-run-cmd`.
|
||||||
|
|||||||
21
readme.md
21
readme.md
@@ -41,30 +41,15 @@ Medios-Macina is a CLI file media manager and toolkit focused on downloading, ta
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>console command</summary>
|
<summary>installation console command</summary>
|
||||||
<pre><code>git clone https://code.glowers.club/goyimnose/Medios-Macina.git
|
<pre><code>git clone https://code.glowers.club/goyimnose/Medios-Macina.git
|
||||||
python Medios-Macina/scripts/bootstrap.py
|
python Medios-Macina/scripts/bootstrap.py
|
||||||
</code></pre>
|
</code></pre>
|
||||||
</details>
|
</details>
|
||||||
|
<br>
|
||||||
|
<b>Start the CLI by simply running "mm"</b>
|
||||||
|
|
||||||
</div>
|
</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)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user