Files
Medios-Macina/SYS/pipeline_executor.py
T

2497 lines
103 KiB
Python

"""
Pipeline execution engine for cmdlet.
"""
from __future__ import annotations
import sys
import time
import re
from datetime import datetime
from typing import Any, Dict, List, Optional, Sequence, Callable
from SYS.cmdlet_catalog import import_cmd_module
from SYS.logger import debug
from SYS.pipeline_state import (
HELP_EXAMPLE_SOURCE_COMMANDS,
_worker,
_cli_parsing,
_result_table,
_split_pipeline_tokens,
_emit_selection_debug_panel,
_is_selectable_table,
)
import logging
logger = logging.getLogger(__name__)
# SYS.rich_display deferred imports are handled inline where needed.
# SYS.background_notifier deferred imports are handled inline where needed.
class PipelineExecutor:
def __init__(self, *, config_loader: Optional[Any] = None) -> None:
self._config_loader = config_loader
self._toolbar_output: Optional[Callable[[str], None]] = None
def _load_config(self) -> Dict[str, Any]:
try:
if self._config_loader is not None:
return self._config_loader.load()
except Exception:
logger.exception(
"Failed to use config_loader.load(); falling back to SYS.config.load_config"
)
try:
from SYS.config import load_config
return load_config()
except Exception:
return {}
def set_toolbar_output(self, output: Optional[Callable[[str], None]]) -> None:
self._toolbar_output = output
# -------------------------------------------------------------------
# Static helpers
# -------------------------------------------------------------------
@staticmethod
def _split_stages(tokens: Sequence[str]) -> List[List[str]]:
stages: List[List[str]] = []
current: List[str] = []
for token in tokens:
if token == "|":
if current:
stages.append(current)
current = []
else:
current.append(token)
if current:
stages.append(current)
return stages
@staticmethod
def _stage_file_action(stage_tokens: Sequence[Any]) -> Optional[str]:
if not stage_tokens:
return None
head = str(stage_tokens[0] or "").replace("_", "-").strip().lower()
if head in {"add-file", "download-file", "delete-file"}:
return head
if head != "file":
return None
args = {str(t).replace("_", "-").strip().lower() for t in stage_tokens[1:]}
if "-add" in args or "--add" in args:
return "add-file"
if "-download" in args or "--download" in args or "-dl" in args or "--dl" in args:
return "download-file"
if "-delete" in args or "--delete" in args or "-del" in args or "--del" in args:
return "delete-file"
return None
@staticmethod
def _validate_download_file_relationship_order(stages: List[List[str]]) -> bool:
def _norm(name: str) -> str:
return str(name or "").replace("_", "-").strip().lower()
def _file_action(stage_tokens: List[str]) -> Optional[str]:
if not stage_tokens:
return None
head = _norm(stage_tokens[0])
if head in {"download-file", "add-file"}:
return head
if head != "file":
return None
args = {_norm(t) for t in stage_tokens[1:]}
if "-download" in args or "--download" in args or "-dl" in args or "--dl" in args:
return "download-file"
if "-add" in args or "--add" in args:
return "add-file"
return None
names: List[str] = []
for stage in stages or []:
if not stage:
continue
names.append(_norm(stage[0]))
dl_idxs = [
i for i, stage in enumerate(stages or []) if _file_action(stage or []) == "download-file"
]
rel_idxs = [i for i, n in enumerate(names) if n == "add-relationship"]
add_file_idxs = [
i for i, stage in enumerate(stages or []) if _file_action(stage or []) == "add-file"
]
if not dl_idxs or not rel_idxs:
return True
for rel_i in rel_idxs:
dl_before = [d for d in dl_idxs if d < rel_i]
if not dl_before:
continue
dl_i = max(dl_before)
if not any(dl_i < a < rel_i for a in add_file_idxs):
print(
"Pipeline order error: when using download-file with add-relationship, "
"add-relationship must come after add-file (so items are stored and have store+hash).\n"
"Example: download-file <...> | add-file -instance <store> | add-relationship\n"
)
return False
return True
@staticmethod
def _try_clear_pipeline_stop(ctx: Any) -> None:
try:
if hasattr(ctx, "clear_pipeline_stop"):
ctx.clear_pipeline_stop()
except Exception:
logger.exception("Failed to clear pipeline stop via ctx.clear_pipeline_stop")
@staticmethod
def _maybe_seed_current_stage_table(ctx: Any) -> None:
try:
if hasattr(ctx, "get_current_stage_table") and not ctx.get_current_stage_table():
display_table = (
ctx.get_display_table() if hasattr(ctx, "get_display_table") else None
)
if display_table:
ctx.set_current_stage_table(display_table)
else:
last_table = (
ctx.get_last_result_table()
if hasattr(ctx, "get_last_result_table")
else None
)
if last_table:
ctx.set_current_stage_table(last_table)
except Exception:
logger.exception("Failed to seed current_stage_table from display or last table")
@staticmethod
def _maybe_apply_pending_pipeline_tail(
ctx: Any, stages: List[List[str]]
) -> List[List[str]]:
try:
pending_tail = (
ctx.get_pending_pipeline_tail()
if hasattr(ctx, "get_pending_pipeline_tail")
else []
)
pending_source = (
ctx.get_pending_pipeline_source()
if hasattr(ctx, "get_pending_pipeline_source")
else None
)
except Exception:
pending_tail = []
pending_source = None
try:
current_source = (
ctx.get_current_stage_table_source_command()
if hasattr(ctx, "get_current_stage_table_source_command")
else None
)
except Exception:
current_source = None
try:
effective_source = current_source or (
ctx.get_last_result_table_source_command()
if hasattr(ctx, "get_last_result_table_source_command")
else None
)
except Exception:
effective_source = current_source
selection_start = bool(stages and stages[0] and stages[0][0].startswith("@"))
def _tail_is_suffix(existing: List[List[str]], tail: List[List[str]]) -> bool:
if not tail or not existing:
return False
if len(tail) > len(existing):
return False
return existing[-len(tail) :] == tail
if pending_tail and selection_start:
if (pending_source is None) or (
effective_source and pending_source == effective_source
):
if not _tail_is_suffix(stages, pending_tail):
stages = list(stages) + list(pending_tail)
try:
if hasattr(ctx, "clear_pending_pipeline_tail"):
ctx.clear_pending_pipeline_tail()
except Exception:
logger.exception(
"Failed to clear pending pipeline tail after appending pending tail"
)
else:
try:
if hasattr(ctx, "clear_pending_pipeline_tail"):
ctx.clear_pending_pipeline_tail()
except Exception:
logger.exception(
"Failed to clear pending pipeline tail (source mismatch branch)"
)
elif pending_tail:
try:
if hasattr(ctx, "clear_pending_pipeline_tail"):
ctx.clear_pending_pipeline_tail()
except Exception:
logger.exception("Failed to clear stale pending pipeline tail")
return stages
def _apply_quiet_background_flag(self, config: Any) -> Any:
if isinstance(config, dict):
try:
is_tty = bool(getattr(sys.stderr, "isatty", lambda: False)())
except Exception:
is_tty = False
config["_quiet_background_output"] = not is_tty
return config
@staticmethod
def _extract_first_stage_selection_tokens(
stages: List[List[str]],
) -> tuple[List[List[str]], List[int], bool, bool]:
first_stage_tokens = stages[0] if stages else []
first_stage_selection_indices: List[int] = []
first_stage_had_extra_args = False
first_stage_select_all = False
if first_stage_tokens:
new_first_stage: List[str] = []
for token in first_stage_tokens:
if token.startswith("@"):
selection = _cli_parsing().SelectionSyntax.parse(token)
if selection is not None:
first_stage_selection_indices = [i - 1 for i in selection]
continue
if token == "@*":
first_stage_select_all = True
continue
new_first_stage.append(token)
if new_first_stage:
stages = list(stages)
stages[0] = new_first_stage
if first_stage_selection_indices or first_stage_select_all:
first_stage_had_extra_args = True
elif first_stage_selection_indices or first_stage_select_all:
stages = list(stages)
stages.pop(0)
return (
stages,
first_stage_selection_indices,
first_stage_had_extra_args,
first_stage_select_all,
)
@staticmethod
def _apply_select_all_if_requested(
ctx: Any, indices: List[int], select_all: bool
) -> List[int]:
if not select_all:
return indices
try:
last_items = ctx.get_last_result_items()
except Exception:
last_items = None
if last_items:
return list(range(len(last_items)))
return indices
@staticmethod
def _maybe_run_class_selector(
ctx: Any,
config: Any,
selected_items: list,
*,
stage_is_last: bool,
source_command: Any = None,
prefer_detail_fallback: bool = False,
) -> bool:
if not stage_is_last:
return False
candidates: list[str] = []
seen: set[str] = set()
current_table = None
table_meta = None
table_type = ""
def _add(value: Any) -> None:
try:
text = str(value or "").strip().lower()
except Exception as exc:
logger.debug(
"Failed to normalize candidate value: %s", exc, exc_info=True
)
return
if not text or text in seen:
return
seen.add(text)
candidates.append(text)
try:
current_table = ctx.get_current_stage_table() or ctx.get_last_result_table()
_add(
current_table.table
if current_table and hasattr(current_table, "table")
else None
)
if current_table and hasattr(current_table, "table"):
table_type = str(getattr(current_table, "table", "") or "").strip()
try:
meta = (
current_table.get_table_metadata()
if current_table is not None
and hasattr(current_table, "get_table_metadata")
else getattr(current_table, "table_metadata", None)
)
except Exception:
meta = None
table_meta = meta if isinstance(meta, dict) else None
if isinstance(meta, dict):
_add(meta.get("plugin"))
except Exception:
logger.exception(
"Failed to inspect current_table/table metadata in _maybe_run_class_selector"
)
for item in selected_items or []:
if isinstance(item, dict):
_add(item.get("plugin"))
_add(item.get("store"))
_add(item.get("table"))
else:
_add(getattr(item, "plugin", None))
_add(getattr(item, "store", None))
_add(getattr(item, "table", None))
try:
from PluginCore.registry import get_plugin, is_known_plugin_name
except Exception:
get_plugin = None # type: ignore
is_known_plugin_name = None # type: ignore
if is_known_plugin_name is not None:
try:
for key in list(candidates):
if not isinstance(key, str):
continue
if "." not in key:
continue
if is_known_plugin_name(key):
continue
prefix = str(key).split(".", 1)[0].strip().lower()
if prefix and is_known_plugin_name(prefix):
_add(prefix)
except Exception:
logger.exception(
"Failed while computing plugin prefix heuristics in _maybe_run_class_selector"
)
if get_plugin is not None:
for key in candidates:
try:
if is_known_plugin_name is not None and (
not is_known_plugin_name(key)
):
continue
except Exception:
logger.exception(
"is_known_plugin_name predicate failed for key %s; falling back",
key,
)
try:
provider = get_plugin(key, config)
except Exception as exc:
logger.exception(
"Failed to load plugin '%s' during selector resolution: %s",
key,
exc,
)
continue
selector = getattr(provider, "selector", None)
if selector is None:
continue
try:
handled = bool(
selector(selected_items, ctx=ctx, stage_is_last=True)
)
except Exception as exc:
logger.exception(
"%s selector failed during selection: %s", key, exc
)
return True
if handled:
return True
if prefer_detail_fallback:
detail_renderer = getattr(provider, "show_selection_details", None)
if callable(detail_renderer):
try:
detail_handled = bool(
detail_renderer(
selected_items,
ctx=ctx,
stage_is_last=True,
source_command=str(source_command or ""),
table_type=table_type,
table_metadata=table_meta,
)
)
except Exception as exc:
logger.exception(
"%s detail fallback failed during selection: %s",
key,
exc,
)
return True
if detail_handled:
return True
store_keys: list[str] = []
for item in selected_items or []:
if isinstance(item, dict):
v = item.get("store")
else:
v = getattr(item, "store", None)
name = str(v or "").strip()
if name:
store_keys.append(name)
if store_keys:
try:
from PluginCore.backend_registry import BackendRegistry
backend_registry = BackendRegistry(config, suppress_debug=True)
_backend_names = list(backend_registry.list_backends() or [])
_backend_by_lower = {
str(n).lower(): str(n) for n in _backend_names if str(n).strip()
}
for name in store_keys:
resolved_name = name
if not backend_registry.is_available(resolved_name):
resolved_name = _backend_by_lower.get(
str(name).lower(), name
)
if not backend_registry.is_available(resolved_name):
continue
backend = backend_registry[resolved_name]
selector = getattr(backend, "selector", None)
if selector is None:
continue
handled = bool(
selector(selected_items, ctx=ctx, stage_is_last=True)
)
if handled:
return True
except Exception:
logger.exception(
"Failed while running store-based selector logic in _maybe_run_class_selector"
)
return False
@staticmethod
def _maybe_expand_plugin_selection(
selected_items: List[Any],
*,
ctx: Any,
config: Dict[str, Any],
stage_table: Any,
) -> Optional[List[Any]]:
candidates: list[str] = []
def _add(value: Any) -> None:
text = str(value or "").strip().lower()
if text and text not in candidates:
candidates.append(text)
table_type = None
try:
table_type = (
stage_table.table
if stage_table is not None and hasattr(stage_table, "table")
else None
)
except Exception:
table_type = None
_add(table_type)
try:
meta = (
stage_table.get_table_metadata()
if stage_table is not None and hasattr(stage_table, "get_table_metadata")
else getattr(stage_table, "table_metadata", None)
)
except Exception:
meta = None
if isinstance(meta, dict):
_add(meta.get("plugin"))
for item in selected_items or []:
if isinstance(item, dict):
_add(item.get("plugin"))
_add(item.get("table"))
_add(item.get("source"))
else:
_add(getattr(item, "plugin", None))
_add(getattr(item, "table", None))
_add(getattr(item, "source", None))
try:
from PluginCore.registry import get_plugin, is_known_plugin_name
except Exception:
return None
for key in list(candidates):
if "." in key:
prefix = str(key).split(".", 1)[0].strip().lower()
if prefix and prefix not in candidates:
candidates.append(prefix)
for key in candidates:
try:
if not is_known_plugin_name(key):
continue
except Exception:
continue
try:
plugin = get_plugin(key, config)
except Exception:
continue
if plugin is None:
continue
expand = getattr(plugin, "expand_selection", None)
if not callable(expand):
continue
try:
expanded = expand(
selected_items,
ctx=ctx,
stage_is_last=False,
table_type=str(table_type or ""),
)
except Exception:
logger.exception("%s expand_selection failed", key)
return None
if expanded:
return list(expanded)
return None
@staticmethod
def _summarize_stage_text(stage_tokens: Sequence[str], limit: int = 140) -> str:
combined = " ".join(str(tok) for tok in stage_tokens if tok is not None).strip()
if not combined:
return ""
normalized = re.sub(r"\s+", " ", combined)
if len(normalized) <= limit:
return normalized
return normalized[: limit - 3].rstrip() + "..."
@staticmethod
def _log_pipeline_event(
worker_manager: Any,
worker_id: Optional[str],
message: str,
) -> None:
if not worker_manager or not worker_id or not message:
return
try:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
except Exception:
timestamp = ""
if timestamp:
text = f"{timestamp} - PIPELINE - {message}"
else:
text = f"PIPELINE - {message}"
try:
worker_manager.append_stdout(worker_id, text + "\n", channel="log")
except Exception:
logger.exception(
"Failed to append pipeline event to worker stdout for %s", worker_id
)
@staticmethod
def _maybe_open_url_selection(
current_table: Any,
selected_items: list,
*,
stage_is_last: bool,
) -> bool:
if not stage_is_last:
return False
if not selected_items or len(selected_items) != 1:
return False
table_type = ""
source_cmd = ""
try:
table_type = (
str(getattr(current_table, "table", "") or "").strip().lower()
)
except Exception:
table_type = ""
try:
source_cmd = (
str(getattr(current_table, "source_command", "") or "")
.strip()
.replace("_", "-")
.lower()
)
except Exception:
source_cmd = ""
if table_type != "url" and source_cmd != "get-url":
return False
item = selected_items[0]
url = None
try:
from SYS.field_access import get_field
url = get_field(item, "url")
except Exception:
try:
url = (
item.get("url")
if isinstance(item, dict)
else getattr(item, "url", None)
)
except Exception:
url = None
url_text = str(url or "").strip()
if not url_text:
return False
try:
import webbrowser
webbrowser.open(url_text, new=2)
return True
except Exception:
return False
# -------------------------------------------------------------------
# Instance helpers
# -------------------------------------------------------------------
def _maybe_enable_background_notifier(
self,
worker_manager: Any,
config: Any,
pipeline_session: Any,
) -> None:
if not (pipeline_session and worker_manager and isinstance(config, dict)):
return
session_worker_ids = config.get("_session_worker_ids")
if not session_worker_ids:
return
try:
output_fn = self._toolbar_output
quiet_mode = bool(config.get("_quiet_background_output"))
terminal_only = quiet_mode and not output_fn
kwargs: Dict[str, Any] = {
"session_worker_ids": session_worker_ids,
"only_terminal_updates": terminal_only,
"overlay_mode": bool(output_fn),
}
if output_fn:
kwargs["output"] = output_fn
from SYS.background_notifier import ensure_background_notifier
ensure_background_notifier(worker_manager, **kwargs)
except Exception:
logger.exception(
"Failed to enable background notifier for session_worker_ids=%r",
session_worker_ids,
)
@staticmethod
def _get_raw_stage_texts(ctx: Any) -> List[str]:
raw_stage_texts: List[str] = []
try:
if hasattr(ctx, "get_current_command_stages"):
raw_stage_texts = ctx.get_current_command_stages() or []
except Exception:
raw_stage_texts = []
return raw_stage_texts
# -------------------------------------------------------------------
# Selection expansion
# -------------------------------------------------------------------
def _maybe_apply_initial_selection(
self,
ctx: Any,
config: Any,
stages: List[List[str]],
*,
selection_indices: List[int],
first_stage_had_extra_args: bool,
worker_manager: Any,
pipeline_session: Any,
) -> tuple[bool, Any]:
if not selection_indices:
return True, None
# ------------------------------------------------------------------
# PHASE 1: Synchronize current stage table with display table
# ------------------------------------------------------------------
display_table = None
try:
display_table = (
ctx.get_display_table() if hasattr(ctx, "get_display_table") else None
)
except Exception:
display_table = None
current_stage_table = None
try:
current_stage_table = (
ctx.get_current_stage_table()
if hasattr(ctx, "get_current_stage_table")
else None
)
except Exception:
current_stage_table = None
try:
if display_table is not None and hasattr(ctx, "set_current_stage_table"):
ctx.set_current_stage_table(display_table)
elif current_stage_table is None and hasattr(ctx, "set_current_stage_table"):
last_table = (
ctx.get_last_result_table()
if hasattr(ctx, "get_last_result_table")
else None
)
if last_table is not None:
ctx.set_current_stage_table(last_table)
except Exception:
logger.exception(
"Failed to sync current_stage_table from display/last table in _maybe_apply_initial_selection"
)
# ------------------------------------------------------------------
# Helper functions for row action/args discovery
# ------------------------------------------------------------------
def _get_row_action(
idx: int, items_cache: Optional[List[Any]] = None
) -> Optional[List[str]]:
try:
action = ctx.get_current_stage_table_row_selection_action(idx)
if action:
return [str(x) for x in action if x is not None]
except Exception:
pass
if items_cache is None:
try:
items_cache = ctx.get_last_result_items() or []
except Exception:
items_cache = []
if 0 <= idx < len(items_cache):
item = items_cache[idx]
if isinstance(item, dict):
candidate = item.get("_selection_action")
if isinstance(candidate, (list, tuple)):
return [str(x) for x in candidate if x is not None]
return None
def _get_row_args(
idx: int, items_cache: Optional[List[Any]] = None
) -> Optional[List[str]]:
try:
args = ctx.get_current_stage_table_row_selection_args(idx)
if args:
return [str(x) for x in args if x is not None]
except Exception:
pass
if items_cache is None:
try:
items_cache = ctx.get_last_result_items() or []
except Exception:
items_cache = []
if 0 <= idx < len(items_cache):
item = items_cache[idx]
if isinstance(item, dict):
candidate = item.get("_selection_args")
if isinstance(candidate, (list, tuple)):
return [str(x) for x in candidate if x is not None]
return None
def _norm_cmd_name(value: Any) -> str:
return str(value or "").replace("_", "-").strip().lower()
# ------------------------------------------------------------------
# PHASE 2: Parse source command and table metadata
# ------------------------------------------------------------------
source_cmd = None
source_args_raw = None
try:
source_cmd = ctx.get_current_stage_table_source_command()
source_args_raw = ctx.get_current_stage_table_source_args()
except Exception:
source_cmd = None
source_args_raw = None
if isinstance(source_args_raw, str):
source_args: List[str] = [source_args_raw]
elif isinstance(source_args_raw, list):
source_args = [str(x) for x in source_args_raw if x is not None]
else:
source_args = []
current_table = None
try:
current_table = ctx.get_current_stage_table()
except Exception:
current_table = None
table_type = (
current_table.table
if current_table and hasattr(current_table, "table")
else None
)
# ------------------------------------------------------------------
# PHASE 3: Handle command expansion for @N syntax
# ------------------------------------------------------------------
command_expanded = False
example_selector_triggered = False
normalized_source_cmd = (
str(source_cmd or "").replace("_", "-").strip().lower()
)
prefer_row_action = False
preferred_row_action = None
if normalized_source_cmd in HELP_EXAMPLE_SOURCE_COMMANDS and selection_indices:
try:
idx = selection_indices[0]
row_args = ctx.get_current_stage_table_row_selection_args(idx)
except Exception:
row_args = None
tokens: List[str] = []
if isinstance(row_args, list) and row_args:
tokens = [str(x) for x in row_args if x is not None]
if tokens:
stage_groups = _split_pipeline_tokens(tokens)
if stage_groups:
for stage in reversed(stage_groups):
stages.insert(0, stage)
selection_indices = []
command_expanded = True
example_selector_triggered = True
if not example_selector_triggered:
if table_type in {"youtube", "soulseek"}:
command_expanded = False
elif source_cmd == "search-file" and source_args and "youtube" in source_args:
command_expanded = False
else:
selected_row_args: List[str] = []
skip_pipe_expansion = source_cmd in {".pipe", ".mpv"} and len(stages) > 0
if len(selection_indices) == 1 and not stages:
try:
row_action = _get_row_action(selection_indices[0])
except Exception:
row_action = None
if row_action:
stages.insert(0, list(row_action))
return True, None
if source_cmd and not skip_pipe_expansion and not prefer_row_action:
src = str(source_cmd).replace("_", "-").strip().lower()
if src == "add-file" and selection_indices:
row_args_list: List[List[str]] = []
for idx in selection_indices:
try:
row_args = ctx.get_current_stage_table_row_selection_args(
idx
)
except Exception:
row_args = None
if isinstance(row_args, list) and row_args:
row_args_list.append(
[str(x) for x in row_args if x is not None]
)
paths: List[str] = []
can_merge = bool(row_args_list) and (
len(row_args_list) == len(selection_indices)
)
if can_merge:
for ra in row_args_list:
if len(ra) == 1:
p = str(ra[0]).strip()
if p:
paths.append(p)
else:
can_merge = False
break
if can_merge and paths:
selected_row_args.append(",".join(paths))
elif len(selection_indices) == 1 and row_args_list:
selected_row_args.extend(row_args_list[0])
else:
if len(selection_indices) == 1:
idx = selection_indices[0]
row_args = ctx.get_current_stage_table_row_selection_args(idx)
if row_args:
selected_row_args.extend(row_args)
if selected_row_args and not stages:
if isinstance(source_cmd, list):
cmd_list: List[str] = [
str(x) for x in source_cmd if x is not None
]
elif isinstance(source_cmd, str):
cmd_list = [source_cmd]
else:
cmd_list = []
expanded_stage: List[str] = (
cmd_list + selected_row_args + source_args
)
stages.insert(0, expanded_stage)
if pipeline_session and worker_manager:
try:
worker_manager.log_step(
pipeline_session.worker_id,
f"@N expansion: {source_cmd} + selected_args={selected_row_args} + source_args={source_args}",
)
except Exception:
logger.exception(
"Failed to record pipeline log step for @N expansion (pipeline_session=%r)",
getattr(pipeline_session, "worker_id", None),
)
elif selected_row_args and stages:
pass
stage_table = None
try:
stage_table = ctx.get_current_stage_table()
except Exception:
stage_table = None
display_table = None
try:
display_table = (
ctx.get_display_table()
if hasattr(ctx, "get_display_table")
else None
)
except Exception:
display_table = None
if not stage_table and display_table is not None:
stage_table = display_table
if not stage_table:
try:
stage_table = ctx.get_last_result_table()
except Exception:
stage_table = None
# ----------------------------------------------------------------
# PHASE 4: Retrieve and filter items from current result set
# ----------------------------------------------------------------
try:
items_list = ctx.get_last_result_items() or []
except Exception as exc:
debug(f"@N: Exception getting items_list: {exc}")
items_list = []
resolved_items = items_list if items_list else []
if items_list:
filtered = [
resolved_items[i]
for i in selection_indices
if 0 <= i < len(resolved_items)
]
if selection_indices:
if len(selection_indices) == 1:
selection_label = f"@{selection_indices[0] + 1}"
else:
selection_label = (
"@{"
+ ",".join(str(idx + 1) for idx in selection_indices)
+ "}"
)
else:
selection_label = "@selection"
_emit_selection_debug_panel(
selection_token=selection_label,
selection_indices=selection_indices,
item_count=len(items_list),
filtered_count=len(filtered),
stage_table_present=(stage_table is not None),
display_table_present=(display_table is not None),
stage_is_last=(not stages),
row_action=preferred_row_action,
downstream_stages=stages,
mode=("row_action" if preferred_row_action else "selection"),
)
if not filtered:
print("No items matched selection in pipeline\n")
return False, None
if stages:
expanded = PipelineExecutor._maybe_expand_plugin_selection(
filtered,
ctx=ctx,
config=config,
stage_table=stage_table,
)
if expanded:
filtered = expanded
if PipelineExecutor._maybe_run_class_selector(
ctx,
config,
filtered,
stage_is_last=(not stages),
source_command=source_cmd,
prefer_detail_fallback=bool(
prefer_row_action
and not stages
and len(selection_indices) == 1
),
):
return False, None
from SYS.pipe_object import coerce_to_pipe_object
filtered_pipe_objs = [
coerce_to_pipe_object(item) for item in filtered
]
piped_result = (
filtered_pipe_objs
if len(filtered_pipe_objs) > 1
else filtered_pipe_objs[0]
)
if pipeline_session and worker_manager:
try:
selection_parts = [f"@{i+1}" for i in selection_indices]
worker_manager.log_step(
pipeline_session.worker_id,
f"Applied @N selection {' | '.join(selection_parts)}",
)
except Exception:
logger.exception(
"Failed to record Applied @N selection log step (pipeline_session=%r)",
getattr(pipeline_session, "worker_id", None),
)
try:
current_table = ctx.get_current_stage_table()
if current_table is None and hasattr(ctx, "get_display_table"):
current_table = ctx.get_display_table()
if current_table is None:
current_table = ctx.get_last_result_table()
except Exception:
logger.exception(
"Failed to determine current_table for selection auto-insert; defaulting to None"
)
current_table = None
table_type_hint = None
try:
raw_table_type = (
stage_table.table
if stage_table is not None and hasattr(stage_table, "table")
else None
)
if isinstance(raw_table_type, str) and raw_table_type.strip():
table_type_hint = raw_table_type
except Exception:
table_type_hint = None
table_type = None
try:
if isinstance(table_type_hint, str) and table_type_hint.strip():
table_type = table_type_hint
else:
table_type = (
current_table.table
if current_table and hasattr(current_table, "table")
else None
)
except Exception:
logger.exception(
"Failed to compute table_type from current_table; using fallback attribute access"
)
table_type = (
current_table.table
if current_table and hasattr(current_table, "table")
else None
)
def _norm_cmd(name: Any) -> str:
return str(name or "").replace("_", "-").strip().lower()
auto_stage = None
if isinstance(table_type, str) and table_type:
try:
from PluginCore.registry import (
selection_auto_stage_for_table,
)
auto_stage = selection_auto_stage_for_table(table_type)
except Exception:
auto_stage = None
source_cmd_for_selection = None
source_args_for_selection: List[str] = []
try:
source_cmd_for_selection = (
ctx.get_current_stage_table_source_command()
or ctx.get_last_result_table_source_command()
)
source_args_for_selection = (
ctx.get_current_stage_table_source_args()
or ctx.get_last_result_table_source_args()
or []
)
except Exception:
source_cmd_for_selection = None
source_args_for_selection = []
if not stages and selection_indices and source_cmd_for_selection:
src_norm = _norm_cmd_name(source_cmd_for_selection)
if src_norm in {".worker", "worker", "workers"}:
if len(selection_indices) == 1:
idx = selection_indices[0]
row_args = _get_row_args(idx, items_list)
if row_args:
stages.append(
[str(source_cmd_for_selection)]
+ row_args
+ [
str(x)
for x in source_args_for_selection
if x is not None
]
)
def _apply_row_action_to_stage(stage_idx: int) -> bool:
if not selection_indices or len(selection_indices) != 1:
return False
row_action = _get_row_action(selection_indices[0], items_list)
if not row_action:
return False
if 0 <= stage_idx < len(stages):
stages[stage_idx] = row_action
return True
return False
# ----------------------------------------------------------------
# PHASE 5: Auto-insert stages based on table type and user selection
# ----------------------------------------------------------------
if not stages:
if isinstance(table_type, str) and table_type.startswith(
"metadata."
):
print("Auto-applying metadata selection via metadata -get")
stages.append(["metadata", "-get"])
elif auto_stage:
try:
print(f"Auto-running selection via {auto_stage[0]}")
except Exception:
logger.exception(
"Failed to print auto-run selection message for %s",
auto_stage[0],
)
stages.append(list(auto_stage))
debug(
f"Inserted auto stage before row action: {stages[-1]}"
)
if selection_indices:
try:
if not _apply_row_action_to_stage(len(stages) - 1):
if len(selection_indices) == 1:
idx = selection_indices[0]
row_args = _get_row_args(idx, items_list)
if row_args:
inserted = stages[-1]
if inserted:
cmd = inserted[0]
tail = [
str(x) for x in inserted[1:]
]
stages[-1] = [cmd] + row_args + tail
except Exception:
logger.exception(
"Failed to attach selection args to auto-inserted stage"
)
if (
not stages
and selection_indices
and len(selection_indices) == 1
):
row_action = _get_row_action(selection_indices[0], items_list)
if row_action:
stages.append(row_action)
if pipeline_session and worker_manager:
try:
worker_manager.log_step(
pipeline_session.worker_id,
f"@N applied row action -> {' '.join(row_action)}",
)
except Exception:
logger.exception(
"Failed to record pipeline log step for applied row action (pipeline_session=%r)",
getattr(
pipeline_session, "worker_id", None
),
)
else:
first_cmd = stages[0][0] if stages and stages[0] else None
first_cmd_norm = _norm_cmd_name(first_cmd)
inserted_provider_download = False
if (
PipelineExecutor._stage_file_action(stages[0])
== "add-file"
):
if len(selection_indices) == 1:
row_action = _get_row_action(
selection_indices[0], items_list
)
if (
row_action
and PipelineExecutor._stage_file_action(row_action)
== "download-file"
):
stages.insert(
0,
[str(x) for x in row_action if x is not None],
)
inserted_provider_download = True
debug(
"Auto-inserting row download-file action before add-file"
)
if (not inserted_provider_download) and len(
selection_indices
) > 1:
try:
has_download_row_action = False
for idx in selection_indices:
row_action = _get_row_action(
idx, items_list
)
if (
row_action
and PipelineExecutor._stage_file_action(
row_action
)
== "download-file"
):
has_download_row_action = True
break
if has_download_row_action:
stages.insert(0, ["file", "-download"])
inserted_provider_download = True
debug(
"Auto-inserting download-file before add-file for provider selection"
)
except Exception:
pass
if (
isinstance(table_type, str)
and table_type.startswith("metadata.")
and first_cmd
not in (
"metadata",
"tag",
".pipe",
".mpv",
)
):
print(
"Auto-inserting metadata -get after metadata selection"
)
stages.insert(0, ["metadata", "-get"])
elif auto_stage:
first_cmd_norm = _norm_cmd_name(
stages[0][0] if stages and stages[0] else None
)
auto_cmd_norm = _norm_cmd_name(auto_stage[0])
if first_cmd_norm not in (
auto_cmd_norm,
".pipe",
".mpv",
):
debug(
f"Auto-inserting {auto_cmd_norm} after selection"
)
stages.insert(0, list(auto_stage))
debug(
f"Inserted auto stage before existing pipeline: {stages[0]}"
)
if selection_indices:
try:
if not _apply_row_action_to_stage(0):
if len(selection_indices) == 1:
idx = selection_indices[0]
row_args = _get_row_args(
idx, items_list
)
if row_args:
inserted = stages[0]
if inserted:
cmd = inserted[0]
tail = [
str(x)
for x in inserted[1:]
]
stages[0] = (
[cmd] + row_args + tail
)
except Exception:
logger.exception(
"Failed to attach selection args to inserted auto stage (alternate branch)"
)
if (not stages) and selection_indices and len(selection_indices) == 1:
try:
selected_item = filtered[0] if filtered else None
if selected_item is not None and not isinstance(
selected_item, dict
):
to_dict = getattr(selected_item, "to_dict", None)
if callable(to_dict):
selected_item = to_dict()
if isinstance(selected_item, dict):
from SYS.rich_display import (
render_item_details_panel,
)
render_item_details_panel(selected_item)
try:
ctx.set_last_result_items_only([selected_item])
except Exception:
pass
except Exception:
logger.exception(
"Failed to render selection-only item details"
)
return True, piped_result
else:
debug("@N: No items to select from (items_list empty)")
print("No previous results to select from\n")
return False, None
return True, None
# -------------------------------------------------------------------
# Live progress
# -------------------------------------------------------------------
@staticmethod
def _maybe_start_live_progress(
config: Any, stages: List[List[str]]
) -> tuple[Any, Dict[int, int]]:
progress_ui = None
pipe_index_by_stage: Dict[int, int] = {}
try:
quiet_mode = (
bool(config.get("_quiet_background_output"))
if isinstance(config, dict)
else False
)
except Exception:
quiet_mode = False
try:
import sys as _sys
if (not quiet_mode) and bool(
getattr(_sys.stderr, "isatty", lambda: False)()
):
from SYS.models import PipelineLiveProgress
pipe_stage_indices: List[int] = []
pipe_labels: List[str] = []
for idx, stage_tokens in enumerate(stages):
if not stage_tokens:
continue
name = str(stage_tokens[0]).replace("_", "-").lower()
if name == "@" or name.startswith("@"):
continue
if (
PipelineExecutor._stage_file_action(stage_tokens)
== "add-file"
or name in {"add_file"}
):
try:
from pathlib import Path as _Path
toks = list(stage_tokens[1:])
i = 0
while i < len(toks):
t = str(toks[i])
low = t.lower().strip()
if low in {"-path", "--path", "-p"} and i + 1 < len(
toks
):
nxt = str(toks[i + 1])
if nxt and ("," not in nxt):
p = _Path(nxt)
if p.exists() and p.is_dir():
name = ""
break
i += 2
continue
i += 1
except Exception:
logger.exception(
"Failed to inspect add-file stage tokens for potential directory; skipping Live progress"
)
if not name:
continue
if name in {"get-relationship", "get-rel"}:
continue
if name in {"get-metadata", "meta"}:
continue
if name in {".pipe", ".mpv"}:
continue
if name == ".matrix":
continue
if (
PipelineExecutor._stage_file_action(stage_tokens)
== "delete-file"
or name in {"del-file"}
):
continue
pipe_stage_indices.append(idx)
pipe_labels.append(name)
if pipe_labels:
progress_ui = PipelineLiveProgress(pipe_labels, enabled=True)
progress_ui.start()
try:
from SYS import pipeline as _pipeline_ctx
if hasattr(_pipeline_ctx, "set_live_progress"):
_pipeline_ctx.set_live_progress(progress_ui)
if hasattr(_pipeline_ctx, "get_progress_event_callback"):
progress_cb = _pipeline_ctx.get_progress_event_callback()
if callable(progress_cb) and hasattr(
progress_ui, "set_event_callback"
):
progress_ui.set_event_callback(progress_cb)
except Exception:
logger.exception(
"Failed to register PipelineLiveProgress with pipeline context"
)
pipe_index_by_stage = {
stage_idx: pipe_idx
for pipe_idx, stage_idx in enumerate(pipe_stage_indices)
}
except Exception:
progress_ui = None
pipe_index_by_stage = {}
return progress_ui, pipe_index_by_stage
# -------------------------------------------------------------------
# Main execution entry point
# -------------------------------------------------------------------
def execute_tokens(self, tokens: List[str]) -> None:
from cmdlet import REGISTRY
from SYS import pipeline_state as ctx
try:
try:
from SYS.logger import debug_panel
debug_panel(
"Pipeline execution",
[
("command", " ".join(str(tok) for tok in tokens)),
("token_count", len(tokens)),
],
)
except Exception:
debug(f"execute_tokens: tokens={tokens}")
self._try_clear_pipeline_stop(ctx)
try:
if hasattr(ctx, "set_current_stage_table"):
ctx.set_current_stage_table(None)
except Exception:
logger.exception(
"Failed to clear current_stage_table in execute_tokens"
)
try:
ctx.store_value("preflight", {})
except Exception:
logger.exception(
"Failed to set preflight cache in execute_tokens"
)
stages = self._split_stages(tokens)
if not stages:
print("Invalid pipeline syntax\n")
return
self._maybe_seed_current_stage_table(ctx)
stages = self._maybe_apply_pending_pipeline_tail(ctx, stages)
config = self._load_config()
config = self._apply_quiet_background_flag(config)
(
stages,
first_stage_selection_indices,
first_stage_had_extra_args,
first_stage_select_all,
) = self._extract_first_stage_selection_tokens(stages)
first_stage_selection_indices = self._apply_select_all_if_requested(
ctx, first_stage_selection_indices, first_stage_select_all
)
piped_result: Any = None
worker_manager = _worker().WorkerManagerRegistry.ensure(config)
pipeline_text = " | ".join(" ".join(stage) for stage in stages)
pipeline_session = _worker().WorkerStages.begin_pipeline(
worker_manager,
pipeline_text=pipeline_text,
config=config,
)
if pipeline_session and worker_manager:
self._log_pipeline_event(
worker_manager,
pipeline_session.worker_id,
f"Pipeline start: {pipeline_text or '(empty pipeline)'}",
)
raw_stage_texts = self._get_raw_stage_texts(ctx)
self._maybe_enable_background_notifier(
worker_manager, config, pipeline_session
)
pipeline_status = "completed"
pipeline_error = ""
progress_ui = None
pipe_index_by_stage: Dict[int, int] = {}
ok, initial_piped = self._maybe_apply_initial_selection(
ctx,
config,
stages,
selection_indices=first_stage_selection_indices,
first_stage_had_extra_args=first_stage_had_extra_args,
worker_manager=worker_manager,
pipeline_session=pipeline_session,
)
if not ok:
return
if initial_piped is not None:
piped_result = initial_piped
if not self._validate_download_file_relationship_order(stages):
pipeline_status = "failed"
pipeline_error = "Invalid pipeline order"
return
progress_ui, pipe_index_by_stage = self._maybe_start_live_progress(
config, stages
)
for stage_index, stage_tokens in enumerate(stages):
if not stage_tokens:
continue
raw_stage_name = str(stage_tokens[0])
cmd_name = raw_stage_name.replace("_", "-").lower()
stage_args = stage_tokens[1:]
if cmd_name == "@":
try:
next_cmd = None
if stage_index + 1 < len(stages) and stages[
stage_index + 1
]:
next_cmd = (
str(stages[stage_index + 1][0])
.replace("_", "-")
.strip()
.lower()
)
current_table = None
try:
current_table = (
ctx.get_current_stage_table()
or ctx.get_last_result_table()
)
except Exception:
current_table = None
source_cmd = (
str(
getattr(current_table, "source_command", "") or ""
)
.replace("_", "-")
.strip()
.lower()
)
is_get_tag_table = source_cmd == "metadata"
if is_get_tag_table and next_cmd == "metadata":
subject = ctx.get_last_result_subject()
if subject is not None:
next_args = [
str(x).replace("_", "-").strip().lower()
for x in (
stages[stage_index + 1][1:]
if stage_index + 1 < len(stages)
else []
)
]
if (
"-add" not in next_args
and "--add" not in next_args
):
pass
else:
piped_result = subject
try:
subject_items = (
subject
if isinstance(subject, list)
else [subject]
)
ctx.set_last_items(subject_items)
except Exception:
logger.exception(
"Failed to set last_items from tag subject during @ handling"
)
if pipeline_session and worker_manager:
try:
worker_manager.log_step(
pipeline_session.worker_id,
"@ used metadata table subject for metadata -add",
)
except Exception:
logger.exception(
"Failed to record pipeline log step for '@ used metadata table subject for metadata -add' (pipeline_session=%r)",
getattr(
pipeline_session,
"worker_id",
None,
),
)
continue
except Exception:
logger.exception(
"Failed to evaluate tag @ subject special-case"
)
last_items = None
try:
last_items = ctx.get_last_result_items()
except Exception:
last_items = None
if last_items:
from SYS.pipe_object import coerce_to_pipe_object
try:
pipe_items = [
coerce_to_pipe_object(x)
for x in list(last_items)
]
except Exception:
pipe_items = list(last_items)
piped_result = (
pipe_items
if len(pipe_items) > 1
else pipe_items[0]
)
try:
ctx.set_last_items(pipe_items)
except Exception:
logger.exception(
"Failed to set last items after @ selection"
)
if pipeline_session and worker_manager:
try:
worker_manager.log_step(
pipeline_session.worker_id,
"@ used last result items",
)
except Exception:
logger.exception(
"Failed to record pipeline log step for '@ used last result items' (pipeline_session=%r)",
getattr(
pipeline_session, "worker_id", None
),
)
continue
subject = ctx.get_last_result_subject()
if subject is None:
print("No current result context available for '@'\n")
pipeline_status = "failed"
pipeline_error = "No result items/subject for @"
return
piped_result = subject
try:
subject_items = (
subject
if isinstance(subject, list)
else [subject]
)
ctx.set_last_items(subject_items)
except Exception:
logger.exception(
"Failed to set last_items from subject during @ handling"
)
if pipeline_session and worker_manager:
try:
worker_manager.log_step(
pipeline_session.worker_id,
"@ used current table subject",
)
except Exception:
logger.exception(
"Failed to record pipeline log step for '@ used current table subject' (pipeline_session=%r)",
getattr(
pipeline_session, "worker_id", None
),
)
continue
if cmd_name.startswith("@"):
selection_token = raw_stage_name
selection = _cli_parsing().SelectionSyntax.parse(
selection_token
)
filter_spec = _cli_parsing().SelectionFilterSyntax.parse(
selection_token
)
is_select_all = selection_token.strip() == "@*"
if (
selection is None
and filter_spec is None
and not is_select_all
):
print(f"Invalid selection: {selection_token}\n")
pipeline_status = "failed"
pipeline_error = f"Invalid selection {selection_token}"
return
selected_indices: List[int] = []
display_table = None
try:
display_table = (
ctx.get_display_table()
if hasattr(ctx, "get_display_table")
else None
)
except Exception:
display_table = None
stage_table = ctx.get_current_stage_table()
try:
if display_table is not None and hasattr(
ctx, "set_current_stage_table"
):
ctx.set_current_stage_table(display_table)
stage_table = display_table
except Exception:
logger.exception(
"Failed to set current_stage_table from display table during selection processing"
)
if not stage_table and display_table is not None:
stage_table = display_table
if not stage_table:
stage_table = ctx.get_last_result_table()
try:
if hasattr(ctx, "debug_table_state"):
ctx.debug_table_state(
f"selection {selection_token}"
)
except Exception:
logger.exception(
"Failed to debug_table_state during selection %s",
selection_token,
)
if display_table is not None and stage_table is display_table:
items_list = ctx.get_last_result_items() or []
else:
if hasattr(ctx, "get_last_selectable_result_items"):
items_list = (
ctx.get_last_selectable_result_items() or []
)
else:
items_list = ctx.get_last_result_items() or []
if is_select_all:
selected_indices = list(range(len(items_list)))
elif filter_spec is not None:
selected_indices = [
i
for i, item in enumerate(items_list)
if _cli_parsing().SelectionFilterSyntax.matches(
item, filter_spec
)
]
else:
selected_indices = [
i - 1 for i in selection
]
resolved_items = items_list if items_list else []
filtered = [
resolved_items[i]
for i in selected_indices
if 0 <= i < len(resolved_items)
]
try:
debug(
f"Selection {selection_token} -> resolved_indices={selected_indices} filtered_count={len(filtered)}"
)
if filtered:
sample = filtered[0]
if isinstance(sample, dict):
debug(
f"Selection sample: hash={sample.get('hash')} store={sample.get('store')} _selection_args={sample.get('_selection_args')} _selection_action={sample.get('_selection_action')}"
)
else:
try:
debug(
f"Selection sample object: provider={getattr(sample, 'provider', None)} store={getattr(sample, 'store', None)}"
)
except Exception:
logger.exception(
"Failed to debug selection sample object"
)
except Exception:
logger.exception(
"Failed to produce selection debug sample for token %s",
selection_token,
)
if not filtered:
print("No items matched selection\n")
pipeline_status = "failed"
pipeline_error = "Empty selection"
return
stage_is_last = stage_index + 1 >= len(stages)
if filter_spec is not None and stage_is_last:
try:
base_table = stage_table
if base_table is None:
base_table = ctx.get_last_result_table()
if base_table is not None and hasattr(
base_table, "copy_with_title"
):
new_table = base_table.copy_with_title(
getattr(base_table, "title", "")
or "Results"
)
else:
new_table = _result_table()(
getattr(base_table, "title", "")
if base_table is not None
else "Results"
)
try:
if base_table is not None and getattr(
base_table, "table", None
):
new_table.set_table(
str(getattr(base_table, "table"))
)
except Exception:
logger.exception(
"Failed to set table on new_table for filter overlay"
)
try:
safe = str(selection_token)[1:].strip()
new_table.set_header_line(
f'filter: "{safe}"'
)
except Exception:
logger.exception(
"Failed to set header line for filter overlay for token %s",
selection_token,
)
for item in filtered:
new_table.add_result(item)
try:
ctx.set_last_result_table_overlay(
new_table,
items=list(filtered),
subject=ctx.get_last_result_subject(),
)
except Exception:
logger.exception(
"Failed to set last_result_table_overlay for filter selection"
)
try:
from SYS.rich_display import (
stdout_console,
)
stdout_console().print()
stdout_console().print(new_table)
except Exception:
logger.exception(
"Failed to render filter overlay to stdout_console"
)
except Exception:
logger.exception(
"Failed while rendering filter overlay for selection %s",
selection_token,
)
continue
current_table = (
ctx.get_current_stage_table()
or ctx.get_last_result_table()
)
if (not is_select_all) and (len(filtered) == 1):
try:
PipelineExecutor._maybe_open_url_selection(
current_table,
filtered,
stage_is_last=(
stage_index + 1 >= len(stages)
),
)
except Exception:
logger.exception(
"Failed to open URL selection for table %s",
getattr(current_table, "table", None),
)
if PipelineExecutor._maybe_run_class_selector(
ctx,
config,
filtered,
stage_is_last=(stage_index + 1 >= len(stages)),
):
return
next_cmd: Optional[str] = None
next_args: List[str] = []
try:
if stage_index + 1 < len(stages) and stages[
stage_index + 1
]:
next_cmd = (
str(stages[stage_index + 1][0])
.replace("_", "-")
.lower()
)
next_args = [
str(x).replace("_", "-").strip().lower()
for x in stages[stage_index + 1][1:]
]
except Exception:
logger.exception(
"Failed to determine next_cmd during selection expansion for stage_index %s",
stage_index,
)
next_cmd = None
next_args = []
def _is_tag_row(obj: Any) -> bool:
try:
if (
hasattr(obj, "__class__")
and obj.__class__.__name__ == "TagItem"
and hasattr(obj, "tag_name")
):
return True
except Exception:
logger.exception(
"Failed to inspect TagItem object while checking _is_tag_row"
)
try:
if isinstance(obj, dict) and obj.get("tag_name"):
return True
except Exception:
logger.exception(
"Failed to inspect dict tag_name while checking _is_tag_row"
)
return False
if (
next_cmd == "tag"
and ("-delete" in next_args or "--delete" in next_args)
and len(filtered) > 1
and all(_is_tag_row(x) for x in filtered)
):
from SYS.field_access import get_field
tags: List[str] = []
first_hash = None
first_store = None
first_path = None
for item in filtered:
tag_name = get_field(item, "tag_name")
if tag_name:
tags.append(str(tag_name))
if first_hash is None:
first_hash = get_field(item, "hash")
if first_store is None:
first_store = get_field(item, "store")
if first_path is None:
first_path = get_field(
item, "path"
) or get_field(item, "target")
if tags:
grouped = {
"table": "tag.selection",
"media_kind": "tag",
"hash": first_hash,
"store": first_store,
"path": first_path,
"tag": tags,
}
piped_result = grouped
continue
from SYS.pipe_object import coerce_to_pipe_object
filtered_pipe_objs = [
coerce_to_pipe_object(item) for item in filtered
]
piped_result = (
filtered_pipe_objs
if len(filtered_pipe_objs) > 1
else filtered_pipe_objs[0]
)
current_table = (
ctx.get_current_stage_table()
or ctx.get_last_result_table()
)
table_type = (
current_table.table
if current_table
and hasattr(current_table, "table")
else None
)
def _norm_stage_cmd(name: Any) -> str:
return (
str(name or "").replace("_", "-").strip().lower()
)
next_cmd = None
if stage_index + 1 < len(stages) and stages[
stage_index + 1
]:
next_cmd = _norm_stage_cmd(
stages[stage_index + 1][0]
)
auto_stage = None
if isinstance(table_type, str) and table_type:
try:
from PluginCore.registry import (
selection_auto_stage_for_table,
)
at_end = bool(stage_index + 1 >= len(stages))
auto_stage = selection_auto_stage_for_table(
table_type,
stage_args if at_end else None,
)
except Exception:
auto_stage = None
if filter_spec is None:
if stage_index + 1 >= len(stages):
if auto_stage:
try:
print(
f"Auto-running selection via {auto_stage[0]}"
)
except Exception:
logger.exception(
"Failed to print auto-run selection message for %s",
auto_stage[0],
)
stages.append(list(auto_stage))
else:
if auto_stage:
auto_cmd = _norm_stage_cmd(auto_stage[0])
if next_cmd not in (
auto_cmd,
".pipe",
".mpv",
):
debug(
f"Auto-inserting {auto_cmd} after selection"
)
stages.insert(
stage_index + 1, list(auto_stage)
)
continue
cmd_fn = REGISTRY.get(cmd_name)
try:
mod = import_cmd_module(cmd_name, reload_loaded=True)
data = getattr(mod, "CMDLET", None) if mod else None
if (
data
and hasattr(data, "exec")
and callable(getattr(data, "exec"))
):
from SYS.cmdlet_spec import (
collect_registered_cmdlet_names,
)
run_fn = getattr(data, "exec")
for registered_name in collect_registered_cmdlet_names(
data, fallback_name=cmd_name
):
REGISTRY[registered_name] = run_fn
cmd_fn = run_fn
except Exception:
pass
if not cmd_fn:
try:
mod = import_cmd_module(cmd_name)
data = getattr(mod, "CMDLET", None) if mod else None
if (
data
and hasattr(data, "exec")
and callable(getattr(data, "exec"))
):
run_fn = getattr(data, "exec")
REGISTRY[cmd_name] = run_fn
cmd_fn = run_fn
except Exception:
cmd_fn = None
if not cmd_fn:
print(f"Unknown command: {cmd_name}\n")
pipeline_status = "failed"
pipeline_error = f"Unknown command: {cmd_name}"
return
try:
from SYS.models import PipelineStageContext
pipe_idx = pipe_index_by_stage.get(stage_index)
output_table: Optional[Any] = None
pre_stage_table: Optional[Any] = None
pre_last_result_table: Optional[Any] = None
session = _worker().WorkerStages.begin_stage(
worker_manager,
cmd_name=cmd_name,
stage_tokens=stage_tokens,
config=config,
command_text=pipeline_text
if pipeline_text
else " ".join(stage_tokens),
)
try:
stage_ctx = PipelineStageContext(
stage_index=stage_index,
total_stages=len(stages),
pipe_index=pipe_idx,
worker_id=session.worker_id
if session
else None,
on_emit=(
lambda x: progress_ui.on_emit(pipe_idx, x)
)
if progress_ui is not None
and pipe_idx is not None
else None,
)
ctx.set_stage_context(stage_ctx)
ctx.set_current_cmdlet_name(cmd_name)
ctx.set_current_stage_text(" ".join(stage_tokens))
ctx.clear_emits()
if progress_ui is not None and pipe_idx is not None:
progress_ui.begin_pipe(pipe_idx, total_items=1)
try:
pre_stage_table = (
ctx.get_current_stage_table()
if hasattr(ctx, "get_current_stage_table")
else None
)
except Exception:
pre_stage_table = None
try:
pre_last_result_table = (
ctx.get_last_result_table()
if hasattr(ctx, "get_last_result_table")
else None
)
except Exception:
pre_last_result_table = None
ret_code = cmd_fn(
piped_result, stage_args, config
)
if ret_code is not None:
try:
normalized_ret = int(ret_code)
except Exception:
normalized_ret = 0
if normalized_ret != 0:
pipeline_status = "failed"
pipeline_error = f"Stage '{cmd_name}' failed with exit code {normalized_ret}"
return
if stage_index + 1 < len(stages):
try:
post_stage_table = (
ctx.get_current_stage_table()
if hasattr(ctx, "get_current_stage_table")
else None
)
except Exception:
post_stage_table = None
if (
post_stage_table is not None
and post_stage_table is not pre_stage_table
):
has_emits = bool(stage_ctx.emits)
if not has_emits:
tail = stages[stage_index + 1 :]
source = None
try:
raw_source = getattr(
post_stage_table,
"source_command",
None,
)
if raw_source:
source = (
str(raw_source)
.replace("_", "-")
.strip()
.lower()
)
except Exception:
source = None
try:
ctx.set_pending_pipeline_tail(
tail, source
)
except Exception:
pass
if post_stage_table is not None:
try:
from SYS.rich_display import (
stdout_console,
)
stdout_console().print()
stdout_console().print(
post_stage_table
)
except Exception:
pass
logger.info(
"Pipeline paused for selection — use @N to continue"
)
pipeline_status = "paused_selection"
return
output_table = None
if stage_index + 1 >= len(stages):
try:
output_table = (
ctx.get_display_table()
if hasattr(ctx, "get_display_table")
else None
)
except Exception:
output_table = None
if output_table is None:
current_stage_table = None
last_result_table = None
try:
current_stage_table = (
ctx.get_current_stage_table()
if hasattr(
ctx, "get_current_stage_table"
)
else None
)
except Exception:
current_stage_table = None
try:
last_result_table = (
ctx.get_last_result_table()
if hasattr(
ctx, "get_last_result_table"
)
else None
)
except Exception:
last_result_table = None
if (
current_stage_table is not None
and current_stage_table
is not pre_stage_table
):
output_table = current_stage_table
elif (
last_result_table is not None
and last_result_table
is not pre_last_result_table
):
output_table = last_result_table
stage_emits = list(stage_ctx.emits)
if stage_emits:
piped_result = (
stage_emits
if len(stage_emits) > 1
else stage_emits[0]
)
else:
piped_result = None
finally:
if progress_ui is not None and pipe_idx is not None:
try:
progress_ui.finish_pipe(pipe_idx)
except Exception:
logger.exception(
"Failed to finish pipe in progress_ui"
)
if output_table is not None:
try:
from SYS.rich_display import (
stdout_console,
)
stdout_console().print()
stdout_console().print(output_table)
except Exception:
logger.exception(
"Failed to render output_table to stdout_console"
)
if session:
try:
session.close()
except Exception:
logger.exception(
"Failed to close pipeline stage session"
)
except Exception as exc:
pipeline_status = "failed"
pipeline_error = f"{cmd_name}: {exc}"
debug(
f"Error in stage {stage_index} ({cmd_name}): {exc}"
)
return
except Exception as exc:
pipeline_status = "failed"
pipeline_error = f"{type(exc).__name__}: {exc}"
print(f"[error] {type(exc).__name__}: {exc}\n")
finally:
if progress_ui is not None:
try:
progress_ui.complete_all_pipes()
except Exception:
logger.exception(
"Failed to complete all pipe UI tasks in progress_ui.complete_all_pipes"
)
try:
progress_ui.stop()
except Exception:
logger.exception("Failed to stop progress_ui")
try:
from SYS import pipeline as _pipeline_ctx
if hasattr(_pipeline_ctx, "set_live_progress"):
_pipeline_ctx.set_live_progress(None)
except Exception:
logger.exception(
"Failed to clear live_progress on pipeline context"
)
try:
if pipeline_session and worker_manager:
pipeline_session.close(
status=pipeline_status, error_msg=pipeline_error
)
except Exception:
logger.exception(
"Failed to close pipeline session during finalization"
)
try:
if pipeline_session and worker_manager:
self._log_pipeline_event(
worker_manager,
pipeline_session.worker_id,
f"Pipeline {pipeline_status}: {pipeline_error or ''}",
)
except Exception:
logger.exception(
"Failed to log final pipeline status (pipeline_session=%r)",
getattr(pipeline_session, "worker_id", None),
)
try:
from SYS.pipeline_state import set_last_execution_result
set_last_execution_result(
status=pipeline_status,
error=pipeline_error,
command_text=pipeline_text,
)
except Exception:
logger.exception(
"Failed to record last execution result for pipeline"
)
__all__ = ["PipelineExecutor"]