updating and refactoring codebase for improved performance and maintainability
This commit is contained in:
+38
-11
@@ -10,18 +10,45 @@ import re
|
||||
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
|
||||
from SYS.logger import debug
|
||||
|
||||
# Prompt-toolkit lexer types are optional at import time; fall back to lightweight
|
||||
# stubs if prompt_toolkit is not available so imports remain safe for testing.
|
||||
try:
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.lexers import Lexer as _PTK_Lexer
|
||||
except Exception: # pragma: no cover - optional dependency
|
||||
Document = object # type: ignore
|
||||
# Fallback to a simple object when prompt_toolkit is not available
|
||||
_PTK_Lexer = object # type: ignore
|
||||
# Prompt-toolkit lexer types are optional and expensive (~300ms). Use find_spec
|
||||
# to detect availability without importing, then lazy-load on first use.
|
||||
import importlib.util as _importlib_util
|
||||
_PTK_AVAILABLE: bool = _importlib_util.find_spec("prompt_toolkit") is not None
|
||||
|
||||
# Expose a stable name used by the rest of the module
|
||||
Lexer = _PTK_Lexer
|
||||
_ptk_Document: Any = None
|
||||
_ptk_Lexer: Any = None
|
||||
|
||||
|
||||
def _get_ptk_Document() -> Any:
|
||||
global _ptk_Document
|
||||
if _ptk_Document is None:
|
||||
if _PTK_AVAILABLE:
|
||||
from prompt_toolkit.document import Document as _Doc
|
||||
_ptk_Document = _Doc
|
||||
else:
|
||||
_ptk_Document = object
|
||||
return _ptk_Document
|
||||
|
||||
|
||||
def _get_ptk_Lexer() -> Any:
|
||||
global _ptk_Lexer
|
||||
if _ptk_Lexer is None:
|
||||
if _PTK_AVAILABLE:
|
||||
from prompt_toolkit.lexers import Lexer as _Lex
|
||||
_ptk_Lexer = _Lex
|
||||
else:
|
||||
_ptk_Lexer = object
|
||||
return _ptk_Lexer
|
||||
|
||||
|
||||
# Stable aliases: these resolve lazily the first time they are accessed.
|
||||
# Code that does `isinstance(x, Document)` or `class Foo(Lexer)` at class-body
|
||||
# time needs the real object, so we keep module-level names that proxy to the
|
||||
# lazy getters via __getattr__ on the module. Callers that reference
|
||||
# Document/Lexer INSIDE functions will always get the real class.
|
||||
# Populate the module-level names now so that class bodies below can inherit.
|
||||
Document: Any = _get_ptk_Document()
|
||||
Lexer: Any = _get_ptk_Lexer()
|
||||
|
||||
# Pre-compiled regexes for the lexer (avoid recompiling on every call)
|
||||
TOKEN_PATTERN = re.compile(
|
||||
|
||||
+23
-12
@@ -1,13 +1,21 @@
|
||||
"""Unified logging utility for automatic file and function name tracking."""
|
||||
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional, Sequence
|
||||
|
||||
from SYS.rich_display import console_for
|
||||
# SYS.rich_display deferred: rich (~100ms) loaded lazily on first log output.
|
||||
_rich_display_mod: Any = None
|
||||
|
||||
|
||||
def _console_for(file):
|
||||
global _rich_display_mod
|
||||
if _rich_display_mod is None:
|
||||
import SYS.rich_display as _m
|
||||
_rich_display_mod = _m
|
||||
return _rich_display_mod.console_for(file)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -73,7 +81,8 @@ def _is_rich_renderable(value: Any) -> bool:
|
||||
|
||||
|
||||
def _caller_location(depth: int = 1) -> tuple[str, str]:
|
||||
frame = inspect.currentframe()
|
||||
import inspect as _inspect
|
||||
frame = _inspect.currentframe()
|
||||
current = frame
|
||||
try:
|
||||
for _ in range(max(0, int(depth))):
|
||||
@@ -160,7 +169,7 @@ def debug(*args, **kwargs) -> None:
|
||||
|
||||
if len(args) == 1 and _is_rich_renderable(args[0]):
|
||||
renderable = args[0]
|
||||
console_for(target_file).print(renderable)
|
||||
_console_for(target_file).print(renderable)
|
||||
file_name, func_name = _caller_location(depth=1)
|
||||
caller_name = f"{file_name}.{func_name}" if file_name and func_name else ""
|
||||
_debug_db_log(caller_name=caller_name, message=f"<rich:{type(renderable).__name__}>")
|
||||
@@ -200,7 +209,8 @@ def debug_inspect(
|
||||
|
||||
# Compute caller prefix (same as log()).
|
||||
prefix = None
|
||||
frame = inspect.currentframe()
|
||||
import inspect as _inspect
|
||||
frame = _inspect.currentframe()
|
||||
if frame is not None and frame.f_back is not None:
|
||||
caller_frame = frame.f_back
|
||||
try:
|
||||
@@ -215,7 +225,7 @@ def debug_inspect(
|
||||
# Render.
|
||||
from rich import inspect as rich_inspect
|
||||
|
||||
console = console_for(file)
|
||||
console = _console_for(file)
|
||||
# If the caller provides a title, treat it as authoritative.
|
||||
# Only fall back to the automatic [file.func] prefix when no title is supplied.
|
||||
effective_title = title
|
||||
@@ -266,12 +276,13 @@ def log(*args, **kwargs) -> None:
|
||||
add_prefix = _DEBUG_ENABLED
|
||||
|
||||
# Get the calling frame
|
||||
frame = inspect.currentframe()
|
||||
import inspect as _inspect
|
||||
frame = _inspect.currentframe()
|
||||
if frame is None:
|
||||
file = kwargs.pop("file", sys.stdout)
|
||||
sep = kwargs.pop("sep", " ")
|
||||
end = kwargs.pop("end", "\n")
|
||||
console_for(file).print(*args, sep=sep, end=end)
|
||||
_console_for(file).print(*args, sep=sep, end=end)
|
||||
return
|
||||
|
||||
caller_frame = frame.f_back
|
||||
@@ -279,7 +290,7 @@ def log(*args, **kwargs) -> None:
|
||||
file = kwargs.pop("file", sys.stdout)
|
||||
sep = kwargs.pop("sep", " ")
|
||||
end = kwargs.pop("end", "\n")
|
||||
console_for(file).print(*args, sep=sep, end=end)
|
||||
_console_for(file).print(*args, sep=sep, end=end)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -302,9 +313,9 @@ def log(*args, **kwargs) -> None:
|
||||
end = kwargs.pop("end", "\n")
|
||||
if add_prefix:
|
||||
prefix = f"[{file_name}.{func_name}]"
|
||||
console_for(file).print(prefix, *args, sep=sep, end=end)
|
||||
_console_for(file).print(prefix, *args, sep=sep, end=end)
|
||||
else:
|
||||
console_for(file).print(*args, sep=sep, end=end)
|
||||
_console_for(file).print(*args, sep=sep, end=end)
|
||||
|
||||
# Log to database if available
|
||||
if _DB_LOGGER:
|
||||
@@ -316,4 +327,4 @@ def log(*args, **kwargs) -> None:
|
||||
pass
|
||||
finally:
|
||||
del frame
|
||||
del caller_frame
|
||||
del caller_frame
|
||||
+80
-56
@@ -1,5 +1,7 @@
|
||||
"""Data models for the pipeline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import inspect
|
||||
@@ -16,23 +18,9 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol, TextIO
|
||||
|
||||
from rich.console import Console
|
||||
from rich.console import ConsoleOptions
|
||||
from rich.console import Group
|
||||
from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
from rich.progress import (
|
||||
BarColumn,
|
||||
DownloadColumn,
|
||||
Progress,
|
||||
SpinnerColumn,
|
||||
TaskID,
|
||||
TaskProgressColumn,
|
||||
TextColumn,
|
||||
TimeRemainingColumn,
|
||||
TimeElapsedColumn,
|
||||
TransferSpeedColumn,
|
||||
)
|
||||
# rich imports are deferred to avoid ~100ms startup cost.
|
||||
# Classes in this module that use rich types (ProgressBar, PipelineLiveProgress)
|
||||
# import them lazily inside their method bodies at first use.
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -440,6 +428,42 @@ def _sanitise_for_json(
|
||||
return repr(value)
|
||||
|
||||
|
||||
def _import_rich() -> Any:
|
||||
"""Lazy-load rich types used by ProgressBar and PipelineLiveProgress."""
|
||||
import rich.console as _rc
|
||||
import rich.live as _rl
|
||||
import rich.panel as _rp
|
||||
import rich.progress as _rprog
|
||||
# Return a namespace-like object with the types we need
|
||||
class _Rich:
|
||||
Console = _rc.Console
|
||||
ConsoleOptions = _rc.ConsoleOptions
|
||||
Group = _rc.Group
|
||||
Live = _rl.Live
|
||||
Panel = _rp.Panel
|
||||
Progress = _rprog.Progress
|
||||
BarColumn = _rprog.BarColumn
|
||||
DownloadColumn = _rprog.DownloadColumn
|
||||
SpinnerColumn = _rprog.SpinnerColumn
|
||||
TaskID = _rprog.TaskID
|
||||
TaskProgressColumn = _rprog.TaskProgressColumn
|
||||
TextColumn = _rprog.TextColumn
|
||||
TimeRemainingColumn = _rprog.TimeRemainingColumn
|
||||
TimeElapsedColumn = _rprog.TimeElapsedColumn
|
||||
TransferSpeedColumn = _rprog.TransferSpeedColumn
|
||||
return _Rich
|
||||
|
||||
|
||||
_rich: Any = None # cached after first call
|
||||
|
||||
|
||||
def _r() -> Any:
|
||||
global _rich
|
||||
if _rich is None:
|
||||
_rich = _import_rich()
|
||||
return _rich
|
||||
|
||||
|
||||
class ProgressBar:
|
||||
"""Rich progress helper for byte-based transfers.
|
||||
|
||||
@@ -521,16 +545,16 @@ class ProgressBar:
|
||||
console = stderr_console()
|
||||
except Exception:
|
||||
logger.exception("Failed to acquire shared stderr Console from SYS.rich_display; using fallback Console")
|
||||
console = Console(file=stream)
|
||||
console = _r().Console(file=stream)
|
||||
else:
|
||||
console = Console(file=stream)
|
||||
progress = Progress(
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
DownloadColumn(),
|
||||
TransferSpeedColumn(),
|
||||
TimeRemainingColumn(),
|
||||
console = _r().Console(file=stream)
|
||||
progress = _r().Progress(
|
||||
_r().TextColumn("[progress.description]{task.description}"),
|
||||
_r().BarColumn(),
|
||||
_r().TaskProgressColumn(),
|
||||
_r().DownloadColumn(),
|
||||
_r().TransferSpeedColumn(),
|
||||
_r().TimeRemainingColumn(),
|
||||
console=console,
|
||||
transient=True,
|
||||
)
|
||||
@@ -867,7 +891,7 @@ class PipelineLiveProgress:
|
||||
overall = self._overall
|
||||
if pipe_progress is None or transfers is None or overall is None:
|
||||
# Not started (or stopped).
|
||||
yield Panel("", title="Pipeline", expand=False)
|
||||
yield _r().Panel("", title="Pipeline", expand=False)
|
||||
return
|
||||
|
||||
body_parts: List[Any] = [pipe_progress]
|
||||
@@ -875,8 +899,8 @@ class PipelineLiveProgress:
|
||||
body_parts.append(status)
|
||||
body_parts.append(transfers)
|
||||
|
||||
yield Group(
|
||||
Panel(Group(*body_parts),
|
||||
yield _r().Group(
|
||||
_r().Panel(_r().Group(*body_parts),
|
||||
title=self._title_text(),
|
||||
expand=False),
|
||||
overall
|
||||
@@ -895,8 +919,8 @@ class PipelineLiveProgress:
|
||||
if status is not None and self._status_tasks:
|
||||
body_parts.append(status)
|
||||
body_parts.append(transfers)
|
||||
return Group(
|
||||
Panel(Group(*body_parts),
|
||||
return _r().Group(
|
||||
_r().Panel(_r().Group(*body_parts),
|
||||
title=self._title_text(),
|
||||
expand=False),
|
||||
overall
|
||||
@@ -911,55 +935,55 @@ class PipelineLiveProgress:
|
||||
# IMPORTANT: use the shared stderr Console instance so that any
|
||||
# `stderr_console().print(...)` calls from inside cmdlets (e.g. preflight
|
||||
# tables/prompts in download-file) cooperate with Rich Live rendering.
|
||||
# If we create a separate Console(file=sys.stderr), output will fight for
|
||||
# If we create a separate _r().Console(file=sys.stderr), output will fight for
|
||||
# terminal cursor control and appear "blocked"/truncated.
|
||||
from SYS.rich_display import stderr_console
|
||||
|
||||
self._console = stderr_console()
|
||||
|
||||
# Persistent per-pipe bars.
|
||||
self._pipe_progress = Progress(
|
||||
TextColumn("{task.description}"),
|
||||
TimeElapsedColumn(),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
self._pipe_progress = _r().Progress(
|
||||
_r().TextColumn("{task.description}"),
|
||||
_r().TimeElapsedColumn(),
|
||||
_r().BarColumn(),
|
||||
_r().TaskProgressColumn(),
|
||||
console=self._console,
|
||||
transient=False,
|
||||
)
|
||||
|
||||
# Transient, per-item spinner for the currently-active subtask.
|
||||
self._subtasks = Progress(
|
||||
TextColumn(" "),
|
||||
SpinnerColumn("simpleDots"),
|
||||
TextColumn("{task.description}"),
|
||||
self._subtasks = _r().Progress(
|
||||
_r().TextColumn(" "),
|
||||
_r().SpinnerColumn("simpleDots"),
|
||||
_r().TextColumn("{task.description}"),
|
||||
console=self._console,
|
||||
transient=False,
|
||||
)
|
||||
|
||||
# Status line below the pipe bars. Kept simple (no extra bar) so it
|
||||
# doesn't visually offset the main pipe bar columns.
|
||||
self._status = Progress(
|
||||
TextColumn(" [bold]└─ {task.description}[/bold]"),
|
||||
self._status = _r().Progress(
|
||||
_r().TextColumn(" [bold]└─ {task.description}[/bold]"),
|
||||
console=self._console,
|
||||
transient=False,
|
||||
)
|
||||
|
||||
# Byte-based transfer bars (download/upload) integrated into the Live view.
|
||||
self._transfers = Progress(
|
||||
TextColumn(" {task.description}"),
|
||||
BarColumn(),
|
||||
TaskProgressColumn(),
|
||||
DownloadColumn(),
|
||||
TransferSpeedColumn(),
|
||||
TimeRemainingColumn(),
|
||||
self._transfers = _r().Progress(
|
||||
_r().TextColumn(" {task.description}"),
|
||||
_r().BarColumn(),
|
||||
_r().TaskProgressColumn(),
|
||||
_r().DownloadColumn(),
|
||||
_r().TransferSpeedColumn(),
|
||||
_r().TimeRemainingColumn(),
|
||||
console=self._console,
|
||||
transient=False,
|
||||
)
|
||||
|
||||
self._overall = Progress(
|
||||
TimeElapsedColumn(),
|
||||
BarColumn(),
|
||||
TextColumn("{task.description}"),
|
||||
self._overall = _r().Progress(
|
||||
_r().TimeElapsedColumn(),
|
||||
_r().BarColumn(),
|
||||
_r().TextColumn("{task.description}"),
|
||||
console=self._console,
|
||||
transient=False,
|
||||
)
|
||||
@@ -982,7 +1006,7 @@ class PipelineLiveProgress:
|
||||
len(self._pipe_labels)),
|
||||
)
|
||||
|
||||
self._live = Live(
|
||||
self._live = _r().Live(
|
||||
self,
|
||||
console=self._console,
|
||||
refresh_per_second=10,
|
||||
@@ -1011,7 +1035,7 @@ class PipelineLiveProgress:
|
||||
# Not initialized yet; start fresh.
|
||||
self.start()
|
||||
return
|
||||
self._live = Live(
|
||||
self._live = _r().Live(
|
||||
self,
|
||||
console=self._console,
|
||||
refresh_per_second=10,
|
||||
|
||||
+49
-35
@@ -13,11 +13,41 @@ from SYS.models import PipelineStageContext
|
||||
from SYS.logger import log, debug, debug_panel, is_debug_enabled
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
from SYS.worker import WorkerManagerRegistry, WorkerStages
|
||||
from SYS.cli_parsing import SelectionSyntax, SelectionFilterSyntax
|
||||
from SYS.rich_display import stdout_console
|
||||
from SYS.background_notifier import ensure_background_notifier
|
||||
from SYS.result_table import Table
|
||||
# SYS.worker deferred: ffmpeg+attr+rich (~260ms) loaded lazily on first pipeline run.
|
||||
_worker_mod: Any = None
|
||||
# SYS.cli_parsing deferred: prompt_toolkit (~300ms) loaded lazily on first selection.
|
||||
_cli_parsing_mod: Any = None
|
||||
|
||||
|
||||
def _worker():
|
||||
global _worker_mod
|
||||
if _worker_mod is None:
|
||||
import SYS.worker as _m
|
||||
_worker_mod = _m
|
||||
return _worker_mod
|
||||
|
||||
|
||||
def _cli_parsing():
|
||||
global _cli_parsing_mod
|
||||
if _cli_parsing_mod is None:
|
||||
import SYS.cli_parsing as _m
|
||||
_cli_parsing_mod = _m
|
||||
return _cli_parsing_mod
|
||||
|
||||
|
||||
# SYS.rich_display deferred: rich (~100ms) loaded lazily on first console output.
|
||||
# SYS.background_notifier deferred: rich/attr/ffmpeg loaded lazily on first notifier use.
|
||||
# SYS.result_table deferred: textual (~140ms) loaded lazily on first Table use.
|
||||
_result_table_mod: Any = None
|
||||
|
||||
|
||||
def _result_table():
|
||||
global _result_table_mod
|
||||
if _result_table_mod is None:
|
||||
from SYS.result_table import Table as _T
|
||||
_result_table_mod = _T
|
||||
return _result_table_mod
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from SYS.cmdlet_catalog import import_cmd_module
|
||||
@@ -680,12 +710,14 @@ def set_last_result_table(
|
||||
"""
|
||||
state = _get_pipeline_state()
|
||||
|
||||
# Push current table to history before replacing
|
||||
# Push current table to history before replacing.
|
||||
# No .copy() needed: last_result_items is about to be replaced by reference,
|
||||
# not mutated in place, so the old list reference is safe to keep in history.
|
||||
if state.last_result_table is not None:
|
||||
state.result_table_history.append(
|
||||
(
|
||||
state.last_result_table,
|
||||
state.last_result_items.copy(),
|
||||
state.last_result_items,
|
||||
state.last_result_subject,
|
||||
)
|
||||
)
|
||||
@@ -724,26 +756,6 @@ def set_last_result_table(
|
||||
logger.exception("Failed to sort result_table and reorder items")
|
||||
|
||||
|
||||
if (
|
||||
result_table is not None
|
||||
and hasattr(result_table, "sort_by_title")
|
||||
and not getattr(result_table, "preserve_order", False)
|
||||
):
|
||||
try:
|
||||
result_table.sort_by_title()
|
||||
# Re-order items list to match the sorted table
|
||||
if state.display_items and hasattr(result_table, "rows"):
|
||||
sorted_items: List[Any] = []
|
||||
for row in result_table.rows:
|
||||
src_idx = getattr(row, "source_index", None)
|
||||
if isinstance(src_idx, int) and 0 <= src_idx < len(state.display_items):
|
||||
sorted_items.append(state.display_items[src_idx])
|
||||
if len(sorted_items) == len(result_table.rows):
|
||||
state.display_items = sorted_items
|
||||
except Exception:
|
||||
logger.exception("Failed to sort overlay result_table and reorder items")
|
||||
|
||||
|
||||
def set_last_result_table_overlay(
|
||||
result_table: Optional[Any],
|
||||
items: Optional[List[Any]] = None,
|
||||
@@ -1423,7 +1435,7 @@ class PipelineExecutor:
|
||||
new_first_stage: List[str] = []
|
||||
for token in first_stage_tokens:
|
||||
if token.startswith("@"): # selection
|
||||
selection = SelectionSyntax.parse(token)
|
||||
selection = _cli_parsing().SelectionSyntax.parse(token)
|
||||
if selection is not None:
|
||||
first_stage_selection_indices = sorted(
|
||||
[i - 1 for i in selection]
|
||||
@@ -1848,6 +1860,7 @@ class PipelineExecutor:
|
||||
}
|
||||
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)
|
||||
@@ -2633,9 +2646,9 @@ class PipelineExecutor:
|
||||
)
|
||||
|
||||
piped_result: Any = None
|
||||
worker_manager = WorkerManagerRegistry.ensure(config)
|
||||
worker_manager = _worker().WorkerManagerRegistry.ensure(config)
|
||||
pipeline_text = " | ".join(" ".join(stage) for stage in stages)
|
||||
pipeline_session = WorkerStages.begin_pipeline(
|
||||
pipeline_session = _worker().WorkerStages.begin_pipeline(
|
||||
worker_manager,
|
||||
pipeline_text=pipeline_text,
|
||||
config=config
|
||||
@@ -2790,8 +2803,8 @@ class PipelineExecutor:
|
||||
|
||||
if cmd_name.startswith("@"): # selection stage
|
||||
selection_token = raw_stage_name
|
||||
selection = SelectionSyntax.parse(selection_token)
|
||||
filter_spec = SelectionFilterSyntax.parse(selection_token)
|
||||
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")
|
||||
@@ -2849,7 +2862,7 @@ class PipelineExecutor:
|
||||
elif filter_spec is not None:
|
||||
selected_indices = [
|
||||
i for i, item in enumerate(items_list)
|
||||
if SelectionFilterSyntax.matches(item, filter_spec)
|
||||
if _cli_parsing().SelectionFilterSyntax.matches(item, filter_spec)
|
||||
]
|
||||
else:
|
||||
selected_indices = sorted(
|
||||
@@ -2894,7 +2907,7 @@ class PipelineExecutor:
|
||||
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 = Table(getattr(base_table, "title", "") if base_table is not None else "Results")
|
||||
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):
|
||||
@@ -2918,6 +2931,7 @@ class PipelineExecutor:
|
||||
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:
|
||||
@@ -3118,7 +3132,7 @@ class PipelineExecutor:
|
||||
pipe_idx = pipe_index_by_stage.get(stage_index)
|
||||
|
||||
overlay_table: Any | None = None
|
||||
session = WorkerStages.begin_stage(
|
||||
session = _worker().WorkerStages.begin_stage(
|
||||
worker_manager,
|
||||
cmd_name=cmd_name,
|
||||
stage_tokens=stage_tokens,
|
||||
|
||||
+71
-44
@@ -18,20 +18,47 @@ from pathlib import Path
|
||||
import json
|
||||
import re
|
||||
|
||||
from rich.box import SIMPLE
|
||||
from rich.console import Group
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Prompt
|
||||
from rich.table import Table as RichTable
|
||||
from rich.text import Text
|
||||
# rich imports are deferred to avoid ~100ms startup cost.
|
||||
# All rich types are only needed inside method bodies, so we lazily import on first use.
|
||||
_rich_mod: Any = None
|
||||
|
||||
# Optional Textual imports - graceful fallback if not available
|
||||
try:
|
||||
from textual.widgets import Tree
|
||||
|
||||
TEXTUAL_AVAILABLE = True
|
||||
except ImportError:
|
||||
TEXTUAL_AVAILABLE = False
|
||||
def _rich():
|
||||
global _rich_mod
|
||||
if _rich_mod is None:
|
||||
import types as _types
|
||||
_m = _types.SimpleNamespace()
|
||||
from rich.box import SIMPLE as _SIMPLE
|
||||
from rich.console import Group as _Group
|
||||
from rich.panel import Panel as _Panel
|
||||
from rich.prompt import Prompt as _Prompt
|
||||
from rich.table import Table as _RichTable
|
||||
from rich.text import Text as _Text
|
||||
_m.SIMPLE = _SIMPLE
|
||||
_m.Group = _Group
|
||||
_m.Panel = _Panel
|
||||
_m.Prompt = _Prompt
|
||||
_m.RichTable = _RichTable
|
||||
_m.Text = _Text
|
||||
_rich_mod = _m
|
||||
return _rich_mod
|
||||
|
||||
|
||||
# Optional Textual imports - lazily loaded to avoid pulling in ~300ms of textual
|
||||
# at import time when the TUI is not being used.
|
||||
import importlib.util as _importlib_util
|
||||
TEXTUAL_AVAILABLE: bool = _importlib_util.find_spec("textual") is not None
|
||||
|
||||
# Tree is populated lazily on first call to build_metadata_tree().
|
||||
_textual_Tree: Any = None
|
||||
|
||||
|
||||
def _get_textual_Tree() -> Any:
|
||||
global _textual_Tree
|
||||
if _textual_Tree is None:
|
||||
from textual.widgets import Tree as _Tree
|
||||
_textual_Tree = _Tree
|
||||
return _textual_Tree
|
||||
|
||||
|
||||
# Import ResultModel from the API for typing; avoid runtime redefinition issues
|
||||
@@ -1591,11 +1618,11 @@ class Table:
|
||||
panel_style = get_result_table_panel_style({"table_appearance": appearance_mode})
|
||||
|
||||
if not self.rows:
|
||||
empty = Text("No results")
|
||||
empty = _rich().Text("No results")
|
||||
return (
|
||||
Panel(
|
||||
_rich().Panel(
|
||||
empty,
|
||||
title=Text(str(self.title), style=header_style),
|
||||
title=_rich().Text(str(self.title), style=header_style),
|
||||
border_style=border_style,
|
||||
padding=(0, 0),
|
||||
expand=False,
|
||||
@@ -1613,7 +1640,7 @@ class Table:
|
||||
seen.add(col.name)
|
||||
col_names.append(col.name)
|
||||
|
||||
table = RichTable(
|
||||
table = _rich().RichTable(
|
||||
show_header=True,
|
||||
header_style=header_style,
|
||||
border_style=border_style,
|
||||
@@ -1661,12 +1688,12 @@ class Table:
|
||||
)
|
||||
|
||||
if self.title or self.header_lines:
|
||||
header_bits = [Text(line) for line in (self.header_lines or [])]
|
||||
renderable = Group(*header_bits, table) if header_bits else table
|
||||
header_bits = [_rich().Text(line) for line in (self.header_lines or [])]
|
||||
renderable = _rich().Group(*header_bits, table) if header_bits else table
|
||||
return (
|
||||
Panel(
|
||||
_rich().Panel(
|
||||
renderable,
|
||||
title=Text(str(self.title), style=header_style),
|
||||
title=_rich().Text(str(self.title), style=header_style),
|
||||
border_style=border_style,
|
||||
padding=(0, 0),
|
||||
expand=False,
|
||||
@@ -1777,7 +1804,7 @@ class Table:
|
||||
from SYS.rich_display import stdout_console
|
||||
|
||||
stdout_console().print(self)
|
||||
stdout_console().print(Panel(Text("Selection is disabled for this table.")))
|
||||
stdout_console().print(_rich().Panel(_rich().Text("Selection is disabled for this table.")))
|
||||
return None
|
||||
|
||||
# Display the table
|
||||
@@ -1789,11 +1816,11 @@ class Table:
|
||||
while True:
|
||||
try:
|
||||
if accept_args:
|
||||
choice = Prompt.ask(
|
||||
choice = _rich().Prompt.ask(
|
||||
f"{prompt} (e.g., '5' or '2 -storage hydrus' or 'q' to quit)"
|
||||
).strip()
|
||||
else:
|
||||
choice = Prompt.ask(
|
||||
choice = _rich().Prompt.ask(
|
||||
f"{prompt} (e.g., '5' or '3-5' or '1,3,5' or 'q' to quit)"
|
||||
).strip()
|
||||
|
||||
@@ -1806,8 +1833,8 @@ class Table:
|
||||
if result is not None:
|
||||
return result
|
||||
stdout_console().print(
|
||||
Panel(
|
||||
Text(
|
||||
_rich().Panel(
|
||||
_rich().Text(
|
||||
"Invalid format. Use: selection (5 or 3-5 or 1,3,5) optionally followed by flags (e.g., '5 -storage hydrus')."
|
||||
)
|
||||
)
|
||||
@@ -1818,8 +1845,8 @@ class Table:
|
||||
if selected_indices is not None:
|
||||
return selected_indices
|
||||
stdout_console().print(
|
||||
Panel(
|
||||
Text(
|
||||
_rich().Panel(
|
||||
_rich().Text(
|
||||
"Invalid format. Use: single (5), range (3-5), list (1,3,5), combined (1-3,7,9-11), or 'q' to quit."
|
||||
)
|
||||
)
|
||||
@@ -1827,16 +1854,16 @@ class Table:
|
||||
except (ValueError, EOFError):
|
||||
if accept_args:
|
||||
stdout_console().print(
|
||||
Panel(
|
||||
Text(
|
||||
_rich().Panel(
|
||||
_rich().Text(
|
||||
"Invalid format. Use: selection (5 or 3-5 or 1,3,5) optionally followed by flags (e.g., '5 -storage hydrus')."
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
stdout_console().print(
|
||||
Panel(
|
||||
Text(
|
||||
_rich().Panel(
|
||||
_rich().Text(
|
||||
"Invalid format. Use: single (5), range (3-5), list (1,3,5), combined (1-3,7,9-11), or 'q' to quit."
|
||||
)
|
||||
)
|
||||
@@ -2468,12 +2495,12 @@ class ItemDetailView(Table):
|
||||
from rich.text import Text
|
||||
|
||||
# 1. Create Detail Grid (matching rich_display.py style)
|
||||
details_table = RichTable.grid(expand=True, padding=(0, 2))
|
||||
details_table = _rich().RichTable.grid(expand=True, padding=(0, 2))
|
||||
details_table.add_column("Key", style="cyan", justify="right", width=15)
|
||||
details_table.add_column("Value", style="white")
|
||||
|
||||
def _render_tag_text(tag_value: Any) -> Text:
|
||||
tag_text = Text()
|
||||
tag_text = _rich().Text()
|
||||
tag_text.append("#", style="dim")
|
||||
|
||||
raw = str(tag_value or "")
|
||||
@@ -2497,17 +2524,17 @@ class ItemDetailView(Table):
|
||||
renderables.append(_render_tag_text(tag))
|
||||
|
||||
if freeform_tags:
|
||||
freeform_grid = RichTable.grid(expand=True, padding=(0, 2))
|
||||
freeform_grid = _rich().RichTable.grid(expand=True, padding=(0, 2))
|
||||
for _ in range(3):
|
||||
freeform_grid.add_column(ratio=1)
|
||||
for row_values in _chunk_detail_tags(freeform_tags, 3):
|
||||
cells = [_render_tag_text(tag) for tag in row_values]
|
||||
while len(cells) < 3:
|
||||
cells.append(Text(""))
|
||||
cells.append(_rich().Text(""))
|
||||
freeform_grid.add_row(*cells)
|
||||
renderables.append(freeform_grid)
|
||||
|
||||
return Group(*renderables)
|
||||
return _rich().Group(*renderables)
|
||||
|
||||
def _has_renderable_value(value: Any) -> bool:
|
||||
if value is None:
|
||||
@@ -2596,9 +2623,9 @@ class ItemDetailView(Table):
|
||||
header_style = get_result_table_header_style()
|
||||
border_style = get_result_table_border_style()
|
||||
detail_title = str(self.detail_title or "Item Details").strip() or "Item Details"
|
||||
elements.append(Panel(
|
||||
elements.append(_rich().Panel(
|
||||
details_table,
|
||||
title=Text(detail_title, style=header_style),
|
||||
title=_rich().Text(detail_title, style=header_style),
|
||||
border_style=border_style,
|
||||
padding=(1, 2)
|
||||
))
|
||||
@@ -2606,10 +2633,10 @@ class ItemDetailView(Table):
|
||||
if results_renderable:
|
||||
# If it's a Panel already (from super().to_rich() with title), use it directly
|
||||
# but force the border style to the result-table standard for consistency
|
||||
if isinstance(results_renderable, Panel):
|
||||
if isinstance(results_renderable, _rich().Panel):
|
||||
results_renderable.border_style = get_result_table_border_style()
|
||||
if results_renderable.title:
|
||||
results_renderable.title = Text(
|
||||
results_renderable.title = _rich().Text(
|
||||
str(results_renderable.title),
|
||||
style=get_result_table_header_style(),
|
||||
)
|
||||
@@ -2622,13 +2649,13 @@ class ItemDetailView(Table):
|
||||
display_title = original_title
|
||||
|
||||
# Add a bit of padding
|
||||
results_group = Group(Text(""), results_renderable, Text(""))
|
||||
results_group = _rich().Group(_rich().Text(""), results_renderable, _rich().Text(""))
|
||||
elements.append(
|
||||
Panel(
|
||||
_rich().Panel(
|
||||
results_group,
|
||||
title=Text(str(display_title), style=get_result_table_header_style()),
|
||||
title=_rich().Text(str(display_title), style=get_result_table_header_style()),
|
||||
border_style=get_result_table_border_style(),
|
||||
)
|
||||
)
|
||||
|
||||
return Group(*elements)
|
||||
return _rich().Group(*elements)
|
||||
|
||||
+30
-21
@@ -13,42 +13,49 @@ import contextlib
|
||||
import sys
|
||||
from typing import Any, Iterator, TextIO, List, Dict, Optional, Tuple, cast
|
||||
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.text import Text
|
||||
from pathlib import Path
|
||||
from SYS.utils import expand_path
|
||||
|
||||
# Configure Rich pretty-printing to avoid truncating long strings (hashes/paths).
|
||||
# This is version-safe: older Rich versions may not support the max_* arguments.
|
||||
try:
|
||||
from rich.pretty import install as _pretty_install
|
||||
# rich imports are deferred to first Console use to avoid ~100ms startup cost.
|
||||
# They are loaded the first time any Console function is called.
|
||||
|
||||
try:
|
||||
_pretty_install(max_string=100_000, max_length=100_000)
|
||||
except TypeError:
|
||||
_pretty_install()
|
||||
except Exception:
|
||||
from SYS.logger import logger
|
||||
logger.exception("Failed to configure rich pretty-printing")
|
||||
|
||||
_STDOUT_CONSOLE = Console(file=sys.stdout)
|
||||
_STDERR_CONSOLE = Console(file=sys.stderr)
|
||||
_STDOUT_CONSOLE: Any = None
|
||||
_STDERR_CONSOLE: Any = None
|
||||
|
||||
|
||||
def stdout_console() -> Console:
|
||||
def _ensure_consoles() -> None:
|
||||
global _STDOUT_CONSOLE, _STDERR_CONSOLE
|
||||
if _STDOUT_CONSOLE is None:
|
||||
from rich.console import Console
|
||||
from rich.pretty import install as _pretty_install
|
||||
try:
|
||||
_pretty_install(max_string=100_000, max_length=100_000)
|
||||
except TypeError:
|
||||
_pretty_install()
|
||||
except Exception:
|
||||
pass
|
||||
_STDOUT_CONSOLE = Console(file=sys.stdout)
|
||||
_STDERR_CONSOLE = Console(file=sys.stderr)
|
||||
|
||||
|
||||
def stdout_console() -> Any:
|
||||
_ensure_consoles()
|
||||
return _STDOUT_CONSOLE
|
||||
|
||||
|
||||
def stderr_console() -> Console:
|
||||
def stderr_console() -> Any:
|
||||
_ensure_consoles()
|
||||
return _STDERR_CONSOLE
|
||||
|
||||
|
||||
def console_for(file: TextIO | None) -> Console:
|
||||
def console_for(file: Any) -> Any:
|
||||
if file is None or file is sys.stdout:
|
||||
_ensure_consoles()
|
||||
return _STDOUT_CONSOLE
|
||||
if file is sys.stderr:
|
||||
_ensure_consoles()
|
||||
return _STDERR_CONSOLE
|
||||
from rich.console import Console
|
||||
return Console(file=file)
|
||||
|
||||
|
||||
@@ -57,7 +64,7 @@ def rprint(renderable: Any = "", *, file: TextIO | None = None) -> None:
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def capture_rich_output(*, stdout: TextIO, stderr: TextIO) -> Iterator[None]:
|
||||
def capture_rich_output(*, stdout: Any, stderr: Any) -> Iterator[None]:
|
||||
"""Temporarily redirect Rich output helpers to provided streams.
|
||||
|
||||
Note: `stdout_console()` / `stderr_console()` use global Console instances,
|
||||
@@ -65,9 +72,11 @@ def capture_rich_output(*, stdout: TextIO, stderr: TextIO) -> Iterator[None]:
|
||||
"""
|
||||
|
||||
global _STDOUT_CONSOLE, _STDERR_CONSOLE
|
||||
_ensure_consoles()
|
||||
|
||||
previous_stdout = _STDOUT_CONSOLE
|
||||
previous_stderr = _STDERR_CONSOLE
|
||||
from rich.console import Console
|
||||
try:
|
||||
_STDOUT_CONSOLE = Console(file=stdout)
|
||||
_STDERR_CONSOLE = Console(file=stderr)
|
||||
|
||||
+19
-7
@@ -4,13 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import subprocess
|
||||
import shutil
|
||||
|
||||
try:
|
||||
import ffmpeg # type: ignore
|
||||
except Exception:
|
||||
ffmpeg = None # type: ignore
|
||||
import os
|
||||
import base64
|
||||
import logging
|
||||
@@ -23,6 +17,22 @@ from urllib.parse import urlparse
|
||||
|
||||
from SYS.utils_constant import mime_maps
|
||||
|
||||
_ffmpeg_mod: Any = None
|
||||
_ffmpeg_checked = False
|
||||
|
||||
|
||||
def _get_ffmpeg():
|
||||
"""Lazily return the ffmpeg module, or None if unavailable."""
|
||||
global _ffmpeg_mod, _ffmpeg_checked
|
||||
if not _ffmpeg_checked:
|
||||
try:
|
||||
import ffmpeg as _f # type: ignore
|
||||
_ffmpeg_mod = _f
|
||||
except Exception:
|
||||
_ffmpeg_mod = None
|
||||
_ffmpeg_checked = True
|
||||
return _ffmpeg_mod
|
||||
|
||||
try:
|
||||
import cbor2
|
||||
except ImportError:
|
||||
@@ -191,6 +201,7 @@ def ffprobe(file_path: str) -> dict:
|
||||
probe = None
|
||||
|
||||
# Try python ffmpeg module first
|
||||
ffmpeg = _get_ffmpeg()
|
||||
if ffmpeg is not None:
|
||||
try:
|
||||
probe = ffmpeg.probe(file_path)
|
||||
@@ -203,7 +214,8 @@ def ffprobe(file_path: str) -> dict:
|
||||
ffprobe_cmd = shutil.which("ffprobe")
|
||||
if ffprobe_cmd:
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
import subprocess as _subprocess
|
||||
proc = _subprocess.run(
|
||||
[
|
||||
ffprobe_cmd,
|
||||
"-v",
|
||||
|
||||
Reference in New Issue
Block a user