feat: add Gallery TUI for interactive configuration and generation

- Implemented a terminal user interface (TUI) for configuring the gallery generator using Textual.
- Included functionality to manage sources with add/remove
- Integrated a log pane to display generation output from subprocess calls.
- Implemented save functionality to persist configuration changes.
This commit is contained in:
Kylian Schmidt
2026-05-08 15:47:02 +02:00
parent 17acd478d3
commit 19fc15abb4
8 changed files with 843 additions and 41 deletions
+4 -8
View File
@@ -248,14 +248,11 @@ export class ExportManager {
// Generate a unique temporary filename
const tempFileName = `export_request_${Date.now()}.json`;
// Get work directory from config or fallback
const workDir = window.galleryConfig?.workDir || '/work/kschmidt/web';
// Save the request to a JSON file that can be picked up by a Python script
const requestData = JSON.stringify(payload, null, 2);
// Show improved export instructions with full command
this.showExportInstructions(requestData, tempFileName, workDir);
this.showExportInstructions(requestData, tempFileName);
}
/**
@@ -274,9 +271,9 @@ export class ExportManager {
/**
* Show export instructions to user
*/
showExportInstructions(requestData, tempFileName, workDir) {
showExportInstructions(requestData, tempFileName) {
const tempFilePath = `/tmp/${tempFileName}`;
const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && cd ${workDir} && python export_plots.py ${tempFilePath}`;
const fullCommand = `echo '${requestData.replace(/'/g, "'\\''")}' > ${tempFilePath} && python export_plots.py ${tempFilePath}`;
const instructions = `
<div class="export-instructions">
@@ -304,7 +301,6 @@ export class ExportManager {
<h4>📋 Command Breakdown:</h4>
<ul>
<li><strong>Creates temporary file:</strong> <code>${tempFilePath}</code></li>
<li><strong>Changes to work directory:</strong> <code>${workDir}</code></li>
<li><strong>Runs export script:</strong> <code>python export_plots.py</code></li>
<li><strong>Output file:</strong> Will be saved in the work directory</li>
</ul>
+34 -10
View File
@@ -14,7 +14,10 @@ from pathlib import Path
import argcomplete
from gallery import generate
from gallery.config import ConfigManager, GalleryConfig, GallerySource, default_config_path
from gallery.config import (
ConfigManager, GalleryConfig, GallerySource,
default_config_path, get_active_config_path, ensure_user_config,
)
_WELCOME = """\
Gallery - Scientific Plot Gallery Generator
@@ -67,7 +70,7 @@ def _is_configured(config_path: Path) -> bool:
def _source_names(prefix, parsed_args, **kwargs):
"""Autocomplete helper: return configured source names."""
try:
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else default_config_path()
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else get_active_config_path()
return [s["name"] for s in ConfigManager(config_path).list_sources()]
except Exception:
return []
@@ -76,7 +79,7 @@ def _source_names(prefix, parsed_args, **kwargs):
def _config_keys(prefix, parsed_args, **kwargs):
"""Autocomplete helper: return known dot-notation config keys."""
try:
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else default_config_path()
config_path = Path(parsed_args.config) if getattr(parsed_args, "config", None) else get_active_config_path()
data = ConfigManager(config_path).list_all()
keys = []
for section, values in data.items():
@@ -94,6 +97,7 @@ def _config_keys(prefix, parsed_args, **kwargs):
# Parser construction (separated so TUI can reuse the structure)
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="gallery",
@@ -135,6 +139,7 @@ def build_parser() -> argparse.ArgumentParser:
cfg_sub = cfg.add_subparsers(dest="action", metavar="ACTION")
sub.add_parser("install-completion", help="Install shell tab-completion (bash/zsh)")
sub.add_parser("tui", help="Launch the interactive TUI")
cfg_sub.add_parser("list", help="Print all config values")
cfg_sub.add_parser("path", help="Print the resolved config file path")
@@ -149,8 +154,9 @@ def build_parser() -> argparse.ArgumentParser:
p_add = cfg_sub.add_parser("add-source", help="Add a plot source")
p_add.add_argument("--name", default=None, help="Source name (default: bottom-level directory name)")
p_add.add_argument("--path", required=True, metavar="DIR",
help="Path to source directory").completer = argcomplete.completers.DirectoriesCompleter()
p_add.add_argument("--path", required=True, metavar="DIR", help="Path to source directory").completer = (
argcomplete.completers.DirectoriesCompleter()
)
p_rm = cfg_sub.add_parser("remove-source", help="Remove a plot source by name")
p_rm.add_argument("name", help="Source name to remove").completer = _source_names
@@ -162,8 +168,9 @@ def build_parser() -> argparse.ArgumentParser:
# Command handlers
# ---------------------------------------------------------------------------
def _run_generate(args: argparse.Namespace) -> int:
config_path = Path(args.config) if args.config else default_config_path()
config_path = Path(args.config) if args.config else get_active_config_path()
try:
config = GalleryConfig.from_yaml(config_path)
@@ -179,8 +186,12 @@ def _run_generate(args: argparse.Namespace) -> int:
else:
source_to_update = matching
success = generate(config=config, clean_first=args.clean, verbose=args.verbose,
source_to_update=source_to_update)
success = generate(
config=config,
clean_first=args.clean,
verbose=args.verbose,
source_to_update=source_to_update,
)
return 0 if success else 1
except (FileNotFoundError, ValueError) as e:
@@ -192,7 +203,13 @@ def _run_generate(args: argparse.Namespace) -> int:
def _run_config(args: argparse.Namespace) -> int:
config_path = Path(args.config) if args.config else default_config_path()
if args.config:
config_path = Path(args.config)
elif args.action in ("set", "add-source", "remove-source"):
# Writes go to the user config; create it from the template if needed
config_path = ensure_user_config()
else:
config_path = get_active_config_path()
mgr = ConfigManager(config_path)
if args.action == "path":
@@ -200,6 +217,7 @@ def _run_config(args: argparse.Namespace) -> int:
elif args.action == "list":
import yaml
print(yaml.dump(mgr.list_all(), default_flow_style=False).rstrip())
elif args.action == "sources":
@@ -280,6 +298,7 @@ def _run_install_completion() -> int:
# Entry point
# ---------------------------------------------------------------------------
def main():
parser = build_parser()
argcomplete.autocomplete(parser) # no-op when not completing; exits during completion
@@ -291,8 +310,13 @@ def main():
sys.exit(_run_config(args))
elif args.command == "install-completion":
sys.exit(_run_install_completion())
elif args.command == "tui":
from gallery.tui import GalleryTUI
config_path = Path(args.config) if args.config else None
GalleryTUI(config_path=config_path).run()
sys.exit(0)
else:
print(_WELCOME.format(config_path=default_config_path()))
print(_WELCOME.format(config_path=get_active_config_path()))
sys.exit(0)
+34 -2
View File
@@ -5,17 +5,50 @@ This module provides dataclasses for managing configuration, including
defaults for gallery generation settings.
"""
import shutil
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Union
import yaml
from platformdirs import user_config_dir
def default_config_path() -> Path:
"""Return the path to the package-bundled default config."""
"""Return the path to the package-bundled read-only template config."""
return Path(__file__).parent / "config.yaml"
def user_config_path() -> Path:
"""Return the user-level config path (~/.config/gallery/config.yaml).
Follows the XDG Base Directory spec via platformdirs:
Linux/macOS → ~/.config/gallery/config.yaml
Windows → %APPDATA%/gallery/config.yaml
"""
return Path(user_config_dir("gallery", appauthor=False)) / "config.yaml"
def get_active_config_path() -> Path:
"""Return the config path to use, with this precedence:
1. User config (~/.config/gallery/config.yaml) — if it exists
2. Package-bundled template — fallback
"""
ucp = user_config_path()
return ucp if ucp.exists() else default_config_path()
def ensure_user_config() -> Path:
"""Ensure ~/.config/gallery/config.yaml exists, creating it from the
package template if needed. Returns the path.
"""
ucp = user_config_path()
if not ucp.exists():
ucp.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(default_config_path(), ucp)
return ucp
def _load_raw(path: Path) -> Dict[str, Any]:
with open(path, "r") as f:
return yaml.safe_load(f) or {}
@@ -182,7 +215,6 @@ class GalleryConfig:
data = {
"paths": {
"work_dir": str(Path.cwd()),
"web_folder": str(self.web_folder),
},
"gallery": {
-1
View File
@@ -343,7 +343,6 @@
window.galleryConfig = {
searchDebounceMs: {{ ui.search_debounce_ms|default(300) }},
maxRecentPlots: {{ ui.max_recent_plots|default(20) }},
workDir: "{{ paths.work_dir }}",
stats: {% if stats %}{{ stats|tojson }}{% else %}null{% endif %}
};
</script>
+449
View File
@@ -0,0 +1,449 @@
"""
Gallery TUI — interactive configuration and generation interface.
Built with Textual (https://textual.textualize.io/).
Key Textual concepts used here:
App — the root class; owns the event loop and screen stack
compose() — declarative method that yields widgets to build the UI tree
Collapsible — a section that can be expanded/collapsed by the user
Input — single-line editable text field
DataTable — scrollable table with rows/columns
Button — clickable button that emits Button.Pressed messages
RichLog — scrollable log pane that accepts Rich markup
reactive — a descriptor that rerenders the UI whenever its value changes
watch_* — method called automatically when a reactive changes
@on — decorator that binds a method to a specific widget message
@work — decorator that runs a method in a background thread,
keeping the UI responsive during long operations
call_from_thread() — safely posts a UI update from a background thread
"""
import subprocess
import sys
from pathlib import Path
from textual import on, work
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Horizontal, ScrollableContainer, Vertical
from textual.reactive import reactive
from textual.widgets import Button, Collapsible, DataTable, Footer, Header, Input, Label, RichLog, Static
from gallery.config import ConfigManager, ensure_user_config, get_active_config_path
# ---------------------------------------------------------------------------
# Field definitions
# Each tuple: (widget_id, config_key, required)
# widget_id is used as the HTML-style id (#web-folder) in CSS selectors
# ---------------------------------------------------------------------------
CONFIG_FIELDS = [
("web-folder", "paths.web_folder", True),
("plot-root", "gallery.plot_root", False),
("png-dpi", "gallery.png_dpi", False),
("backup-folder", "gallery.backup_folder", False),
("cache-enabled", "metadata.cache_enabled", False),
("inherit-meta", "metadata.inherit_from_parent",False),
]
REQUIRED_IDS = {fid for fid, _, req in CONFIG_FIELDS if req}
class GalleryTUI(App):
"""Single-screen TUI for configuring and running the gallery generator."""
TITLE = "Gallery"
SUB_TITLE = "Scientific Plot Gallery Generator"
# DEFAULT_CSS is Textual's inline stylesheet (TCSS — a CSS subset).
# Each rule targets widgets by type, id (#), or class (.).
DEFAULT_CSS = """
/* Main scrollable area fills all available space */
ScrollableContainer {
height: 1fr;
padding: 0 1;
}
/* Active config file path shown at top */
#config-path-label {
color: $text-muted;
margin-bottom: 1;
}
/* Breathing room between collapsible sections */
Collapsible {
margin-bottom: 1;
}
/* One-line field rows — 3 cells tall (border + content + border), no gap */
.field-row {
height: 3;
margin-bottom: 0;
}
.field-row Label {
width: 16;
padding-top: 1;
color: $text-muted;
}
.field-row Input {
width: 1fr;
height: 3;
}
/* Red border on required inputs that are empty */
.required-empty {
border: tall $error;
}
/* Sources table */
#sources-table {
height: 8;
margin-bottom: 1;
}
/* Add-source inline form (hidden by default) */
#add-source-form {
display: none;
height: auto;
border: tall $accent;
padding: 1;
margin-bottom: 1;
}
#add-source-form.visible {
display: block;
}
#add-source-form Label {
color: $text-muted;
margin-bottom: 0;
}
#add-source-form Input {
margin-bottom: 1;
}
#add-source-form Horizontal {
height: auto;
align: right middle;
}
#add-source-form Button {
margin-left: 1;
}
/* Generate log pane */
#generate-log {
height: 12;
}
/* Bottom action bar — docked so it is always visible regardless of
how tall the ScrollableContainer grows */
#footer-bar {
dock: bottom;
height: 3;
align: right middle;
padding: 0 1;
border-top: solid $accent;
}
#footer-bar Button {
margin-left: 2;
}
/* Dirty indicator shown in subtitle */
#dirty-indicator {
color: $warning;
text-style: bold;
}
"""
# BINDINGS wires keyboard shortcuts to action_* methods.
# Entries with show=True appear in the Footer widget automatically.
# ctrl+S (capital S) is how Textual represents Ctrl+Shift+S.
BINDINGS = [
Binding("ctrl+s", "save_config", "Save Config"),
Binding("ctrl+S", "save_config", show=False), # Ctrl+Shift+S alias
]
# reactive is a Textual descriptor. When `dirty` changes value, Textual
# automatically calls `watch_dirty()` and re-renders any widget that
# depends on it.
dirty: reactive[bool] = reactive(False)
def __init__(self, config_path: Path | None = None):
super().__init__()
self.config_path = Path(config_path) if config_path else get_active_config_path()
self.mgr = ConfigManager(self.config_path)
# Local copy of sources so we can diff on save
self._sources: list[dict] = []
# ------------------------------------------------------------------
# compose() — builds the widget tree declaratively.
# Textual calls this once at startup; yield order = render order.
# ------------------------------------------------------------------
def compose(self) -> ComposeResult:
yield Header()
with ScrollableContainer():
# Config file path — read-only info line
yield Static(id="config-path-label")
# ── Paths ───────────────────────────────────────────────
# Collapsible wraps any widgets in a togglable section.
with Collapsible(title="Paths", collapsed=False):
with Horizontal(classes="field-row"):
yield Label("Web folder")
yield Input(
id="web-folder",
placeholder="required — /path/to/public_html",
)
# ── Gallery settings ────────────────────────────────────
with Collapsible(title="Gallery Settings", collapsed=False):
with Horizontal(classes="field-row"):
yield Label("Plot root")
yield Input(id="plot-root", placeholder="gallery")
with Horizontal(classes="field-row"):
yield Label("PNG DPI")
yield Input(id="png-dpi", placeholder="400")
with Horizontal(classes="field-row"):
yield Label("Backup folder")
yield Input(id="backup-folder", placeholder="leave empty to disable")
with Horizontal(classes="field-row"):
yield Label("Cache metadata")
yield Input(id="cache-enabled", placeholder="true")
with Horizontal(classes="field-row"):
yield Label("Inherit meta")
yield Input(id="inherit-meta", placeholder="true")
# ── Sources ─────────────────────────────────────────────
with Collapsible(title="Sources", collapsed=False):
# DataTable renders a scrollable grid.
# Rows are added in on_mount() once the widget exists.
yield DataTable(id="sources-table", cursor_type="row")
with Horizontal():
yield Button("+ Add Source", id="add-source-btn", variant="primary")
yield Button(" Remove Selected", id="remove-source-btn", variant="warning")
# Inline add-source form, hidden until user clicks "Add Source"
with Vertical(id="add-source-form"):
yield Label("Name (optional — defaults to directory name)")
yield Input(id="new-name", placeholder="e.g. my_plots")
yield Label("Path")
yield Input(id="new-path", placeholder="/absolute/path/to/plots")
with Horizontal():
yield Button("Confirm", id="confirm-add", variant="success")
yield Button("Cancel", id="cancel-add")
# ── Generate log ────────────────────────────────────────
with Collapsible(title="Generate Log", collapsed=False):
# RichLog accepts Rich markup and plain text.
# We stream subprocess output here line by line.
yield RichLog(id="generate-log", highlight=True, markup=True)
# Bottom bar — always visible outside the scroll area
with Horizontal(id="footer-bar"):
yield Static("", id="dirty-indicator")
yield Button("Save Config", id="save-btn", variant="success")
yield Button("Generate", id="generate-btn", variant="primary")
yield Footer()
# ------------------------------------------------------------------
# on_mount — called once after compose(); safe to query widgets here
# ------------------------------------------------------------------
def on_mount(self) -> None:
self._setup_sources_table()
self._load_config_into_fields()
self.query_one("#config-path-label", Static).update(
f"Config: {self.config_path}"
)
def _setup_sources_table(self) -> None:
table: DataTable = self.query_one("#sources-table", DataTable)
table.add_columns("Name", "Path")
def _load_config_into_fields(self) -> None:
"""Read config file and populate every Input widget."""
for widget_id, config_key, _required in CONFIG_FIELDS:
try:
value = self.mgr.get(config_key)
inp: Input = self.query_one(f"#{widget_id}", Input)
inp.value = str(value) if value is not None else ""
except Exception:
pass
# Validate required fields on load
self._validate_required()
# Populate sources table
self._sources = list(self.mgr.list_sources())
self._refresh_sources_table()
# Config is freshly loaded — not dirty yet
self.dirty = False
def _refresh_sources_table(self) -> None:
table: DataTable = self.query_one("#sources-table", DataTable)
table.clear()
for s in self._sources:
table.add_row(s.get("name", ""), s.get("path", ""))
def _validate_required(self) -> None:
"""Add/remove .required-empty CSS class on required inputs."""
for widget_id in REQUIRED_IDS:
try:
inp: Input = self.query_one(f"#{widget_id}", Input)
# add_class / remove_class toggle CSS classes on a widget
if not inp.value.strip():
inp.add_class("required-empty")
else:
inp.remove_class("required-empty")
except Exception:
pass
# ------------------------------------------------------------------
# watch_dirty — Textual calls this automatically whenever `dirty` changes.
# Naming convention: watch_<reactive_name>
# ------------------------------------------------------------------
def watch_dirty(self, value: bool) -> None:
indicator: Static = self.query_one("#dirty-indicator", Static)
indicator.update("● unsaved changes" if value else "")
# ------------------------------------------------------------------
# Event handlers — @on(MessageType, "#widget-id") binds a method to a
# specific message from a specific widget (or any widget of that type).
# ------------------------------------------------------------------
@on(Input.Changed)
def _on_any_input_changed(self, event: Input.Changed) -> None:
"""Mark config dirty and re-validate required fields on every keystroke."""
self.dirty = True
self._validate_required()
# -- Save ------------------------------------------------------------
# action_* methods are called by BINDINGS — same name after "action_"
def action_save_config(self) -> None:
self._save_config()
@on(Button.Pressed, "#save-btn")
def _save_config(self) -> None:
"""Write all field values back to the config file via ConfigManager.
On first save the package template is copied to ~/.config/gallery/config.yaml
so that the package-bundled file is never modified.
"""
# ensure_user_config() copies the template to ~/.config/gallery/config.yaml
# if it doesn't exist yet, then returns that path.
self.config_path = ensure_user_config()
self.mgr = ConfigManager(self.config_path)
self.query_one("#config-path-label", Static).update(f"Config: {self.config_path}")
for widget_id, config_key, _ in CONFIG_FIELDS:
try:
inp: Input = self.query_one(f"#{widget_id}", Input)
self.mgr.set(config_key, inp.value.strip() or '""')
except Exception as exc:
self.notify(f"Could not save {config_key}: {exc}", severity="error")
return
# Sync sources: remove all then re-add from in-memory list.
# ConfigManager.set("sources", ...) would lose the list structure,
# so we use the dedicated source helpers instead.
try:
for s in self.mgr.list_sources():
self.mgr.remove_source(s["name"])
for s in self._sources:
self.mgr.add_source(s["name"], s["path"])
except Exception as exc:
self.notify(f"Could not save sources: {exc}", severity="error")
return
self.dirty = False
# notify() shows a transient toast message at the bottom of the screen
self.notify("Config saved.", severity="information")
# -- Add source form -------------------------------------------------
@on(Button.Pressed, "#add-source-btn")
def _show_add_form(self) -> None:
"""Reveal the inline add-source form."""
form = self.query_one("#add-source-form")
form.add_class("visible")
self.query_one("#new-path", Input).focus()
@on(Button.Pressed, "#cancel-add")
def _hide_add_form(self) -> None:
form = self.query_one("#add-source-form")
form.remove_class("visible")
self.query_one("#new-name", Input).value = ""
self.query_one("#new-path", Input).value = ""
@on(Button.Pressed, "#confirm-add")
def _confirm_add_source(self) -> None:
path_str = self.query_one("#new-path", Input).value.strip()
name_str = self.query_one("#new-name", Input).value.strip()
if not path_str:
self.notify("Path is required.", severity="warning")
return
name = name_str or Path(path_str).resolve().name
if any(s["name"] == name for s in self._sources):
self.notify(f"Source '{name}' already exists.", severity="warning")
return
self._sources.append({"name": name, "path": path_str})
self._refresh_sources_table()
self._hide_add_form()
self.dirty = True
# -- Remove source ---------------------------------------------------
@on(Button.Pressed, "#remove-source-btn")
def _remove_selected_source(self) -> None:
table: DataTable = self.query_one("#sources-table", DataTable)
# cursor_row is the index of the highlighted row
if table.cursor_row is None or not self._sources:
self.notify("Select a row first.", severity="warning")
return
idx = table.cursor_row
if idx < len(self._sources):
removed = self._sources.pop(idx)
self._refresh_sources_table()
self.dirty = True
self.notify(f"Removed source '{removed['name']}'.")
# -- Generate --------------------------------------------------------
@on(Button.Pressed, "#generate-btn")
def _start_generate(self) -> None:
log: RichLog = self.query_one("#generate-log", RichLog)
log.clear()
log.write("[bold green]Starting generation...[/bold green]")
# Expand the log collapsible so output is visible
for c in self.query(Collapsible):
if "Generate Log" in str(c.title):
c.collapsed = False
self._generate_worker()
# @work(thread=True) runs the decorated method in a background thread.
# Without this, the subprocess call would block the UI event loop and
# the screen would freeze until generation finishes.
@work(thread=True)
def _generate_worker(self) -> None:
log: RichLog = self.query_one("#generate-log", RichLog)
cmd = [sys.executable, "-m", "gallery.cli", "generate",
"--config", str(self.config_path), "--verbose"]
try:
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
for line in proc.stdout:
line = line.rstrip()
if line:
# call_from_thread() is required when touching UI widgets
# from a background thread — Textual's event loop is not
# thread-safe, so all UI mutations must go through this.
self.call_from_thread(log.write, line)
proc.wait()
msg = "[bold green]Done.[/bold green]" if proc.returncode == 0 \
else f"[bold red]Failed (exit {proc.returncode}).[/bold red]"
self.call_from_thread(log.write, msg)
except Exception as exc:
self.call_from_thread(log.write, f"[bold red]Error: {exc}[/bold red]")
-1
View File
@@ -160,7 +160,6 @@ def render_gallery_page(
output_html = web_dir / "index.html"
with output_html.open("w") as f:
paths_dict = {
"work_dir": str(Path.cwd()),
"web_folder": str(config.web_folder),
}
ui_dict = {
+3 -1
View File
@@ -37,7 +37,9 @@ dependencies = [
"Jinja2>=3.0.0",
"PyYAML>=5.0",
"argcomplete>=3.0",
"platformdirs>=3.0",
"pytest",
"textual>=0.50",
]
[project.optional-dependencies]
@@ -66,4 +68,4 @@ line_length = 120
[tool.flake8]
max-line-length = 120
extend-ignore = ["E203", "W503"]
extend-ignore = ["E203", "W503"]
Generated
+319 -18
View File
@@ -7,7 +7,8 @@ resolution-markers = [
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
[[package]]
@@ -24,7 +25,8 @@ name = "astroid"
version = "3.2.4"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
@@ -72,7 +74,8 @@ name = "black"
version = "24.8.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
@@ -210,7 +213,8 @@ version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
@@ -252,7 +256,8 @@ name = "dill"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" }
wheels = [
@@ -295,10 +300,16 @@ source = { editable = "." }
dependencies = [
{ name = "argcomplete" },
{ name = "jinja2" },
{ name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "pytest", version = "9.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pyyaml" },
{ name = "textual", version = "0.73.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" },
{ name = "textual", version = "6.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
{ name = "textual", version = "8.2.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
[package.optional-dependencies]
@@ -323,10 +334,12 @@ requires-dist = [
{ name = "black", marker = "extra == 'dev'", specifier = ">=22.0" },
{ name = "jinja2", specifier = ">=3.0.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=0.900" },
{ name = "platformdirs", specifier = ">=3.0" },
{ name = "pylint", marker = "extra == 'dev'", specifier = ">=2.0" },
{ name = "pytest" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" },
{ name = "pyyaml", specifier = ">=5.0" },
{ name = "textual", specifier = ">=0.50" },
]
provides-extras = ["dev"]
@@ -348,7 +361,8 @@ version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
@@ -375,7 +389,8 @@ name = "isort"
version = "5.13.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/87/f9/c1eb8635a24e87ade2efce21e3ce8cd6b8630bb685ddc9cdaca1349b2eb5/isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", size = 175303, upload-time = "2023-12-13T20:37:26.124Z" }
wheels = [
@@ -522,12 +537,96 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/f3/c017fe4337e263bac6a38d2768d687c06e82886d6c131c99179063006323/librt-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:e45e46ff5fdfc690e77bb8557d5ba56974c4006b744ddbd70cce99fec6bfbeb8", size = 70725, upload-time = "2026-05-05T16:31:22.182Z" },
]
[[package]]
name = "linkify-it-py"
version = "2.0.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "uc-micro-py", version = "1.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
]
[[package]]
name = "linkify-it-py"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "uc-micro-py", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "mdurl", marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" },
]
[package.optional-dependencies]
linkify = [
{ name = "linkify-it-py", version = "2.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
]
plugins = [
{ name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
]
[[package]]
name = "markdown-it-py"
version = "4.2.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "mdurl", marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
]
[package.optional-dependencies]
linkify = [
{ name = "linkify-it-py", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
]
[[package]]
name = "markupsafe"
version = "2.1.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" }
wheels = [
@@ -695,12 +794,57 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
]
[[package]]
name = "mdit-py-plugins"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/03/a2ecab526543b152300717cf232bb4bb8605b6edb946c845016fa9c9c9fd/mdit_py_plugins-0.4.2.tar.gz", hash = "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5", size = 43542, upload-time = "2024-09-09T20:27:49.564Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/f7/7782a043553ee469c1ff49cfa1cdace2d6bf99a1f333cf38676b3ddf30da/mdit_py_plugins-0.4.2-py3-none-any.whl", hash = "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", size = 55316, upload-time = "2024-09-09T20:27:48.397Z" },
]
[[package]]
name = "mdit-py-plugins"
version = "0.6.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
]
dependencies = [
{ name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/3d/e0e8d9d1cee04f758120915e2b2a3a07eb41f8cf4654b4734788a522bcd1/mdit_py_plugins-0.6.0.tar.gz", hash = "sha256:2436f14a7295837ac9228a36feeabda867c4abc488c8d019ad5c0bda88eee040", size = 56025, upload-time = "2026-05-07T12:20:42.295Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/d6/48f5b9e44e2e760855d7b489b1317cd7620e82dcb73197961e5cc1391348/mdit_py_plugins-0.6.0-py3-none-any.whl", hash = "sha256:f7e7a25d8b616fee99cb1e330da73451d11a8061baf39bb9663ab9ce0e005b90", size = 66655, upload-time = "2026-05-07T12:20:41.226Z" },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
[[package]]
name = "mypy"
version = "1.14.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "mypy-extensions", marker = "python_full_version < '3.9'" },
@@ -890,7 +1034,8 @@ name = "pathspec"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" }
wheels = [
@@ -918,7 +1063,8 @@ name = "platformdirs"
version = "4.3.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302, upload-time = "2024-09-17T19:06:50.688Z" }
wheels = [
@@ -957,7 +1103,8 @@ name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
wheels = [
@@ -980,10 +1127,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]]
name = "pygments"
version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
"python_full_version == '3.9.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
@@ -994,7 +1161,8 @@ name = "pylint"
version = "3.2.7"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "astroid", version = "3.2.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
@@ -1065,7 +1233,8 @@ name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
@@ -1093,7 +1262,7 @@ dependencies = [
{ name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "packaging", marker = "python_full_version == '3.9.*'" },
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "pygments", marker = "python_full_version == '3.9.*'" },
{ name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "tomli", marker = "python_full_version == '3.9.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
@@ -1117,7 +1286,7 @@ dependencies = [
{ name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "packaging", marker = "python_full_version >= '3.10'" },
{ name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pygments", marker = "python_full_version >= '3.10'" },
{ name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
@@ -1254,6 +1423,107 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0c/25113e0b5e103d7f1490c0e947e303fe4a696c10b501dea7a9f49d4e876c/pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007", size = 158777, upload-time = "2025-09-25T21:33:15.55Z" },
]
[[package]]
name = "rich"
version = "14.3.4"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
{ name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" },
]
[[package]]
name = "rich"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
"python_full_version == '3.9.*'",
]
dependencies = [
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
]
[[package]]
name = "textual"
version = "0.73.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.8.1'",
]
dependencies = [
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify", "plugins"], marker = "python_full_version < '3.8.1'" },
{ name = "rich", version = "14.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" },
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8.1'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d8/e9/4939bf72d4a7d1a37aa5d55ad4438594a9d5e59875195dd89e9d8c14a9a9/textual-0.73.0.tar.gz", hash = "sha256:ccd1e873370577f557dfdf2b3411f2a4f68b57d4365f9d83a00d084afb15f5a6", size = 1291992, upload-time = "2024-07-18T15:42:55.233Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/f3/62ec72b437647787ac7305699e7e00318fd25827212a6b5b7fbb278ec17d/textual-0.73.0-py3-none-any.whl", hash = "sha256:4d93d80d203f7fb7ba51828a546e8777019700d529a1b405ceee313dea2edfc2", size = 564394, upload-time = "2024-07-18T15:42:52.883Z" },
]
[[package]]
name = "textual"
version = "6.2.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
]
dependencies = [
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify", "plugins"], marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
{ name = "platformdirs", version = "4.3.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
{ name = "pygments", version = "2.19.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
{ name = "rich", version = "14.3.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.8.1' and python_full_version < '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a2/30/38b615f7d4b16f6fdd73e4dcd8913e2d880bbb655e68a076e3d91181a7ee/textual-6.2.1.tar.gz", hash = "sha256:4699d8dfae43503b9c417bd2a6fb0da1c89e323fe91c4baa012f9298acaa83e1", size = 1570645, upload-time = "2025-10-01T16:11:24.467Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c5/93/02c7adec57a594af28388d85da9972703a4af94ae1399542555cd9581952/textual-6.2.1-py3-none-any.whl", hash = "sha256:3c7190633cd4d8bfe6049ae66808b98da91ded2edb85cef54e82bf77b03d2a54", size = 710702, upload-time = "2025-10-01T16:11:22.161Z" },
]
[[package]]
name = "textual"
version = "8.2.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
"python_full_version == '3.9.*'",
]
dependencies = [
{ name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version == '3.9.*'" },
{ name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, extra = ["linkify"], marker = "python_full_version >= '3.10'" },
{ name = "mdit-py-plugins", version = "0.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "mdit-py-plugins", version = "0.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "platformdirs", version = "4.4.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
{ name = "platformdirs", version = "4.9.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
{ name = "pygments", version = "2.20.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "rich", version = "15.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/62/1e/1eedc5bac184d00aaa5f9a99095f7e266af3ec46fa926c1051be5d358da1/textual-8.2.5.tar.gz", hash = "sha256:6c894e65a879dadb4f6cf46ddcfedb0173ff7e0cb1fe605ff7b357a597bdbc90", size = 1851596, upload-time = "2026-04-30T08:02:58.956Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/01/c4555f9c8a692ff83d84930150540f743ce94c89234f9e9a15ff4baba3a8/textual-8.2.5-py3-none-any.whl", hash = "sha256:247d2aa2faf222749c321f88a736247f37ee2c023604079c7490bfacddfcd4b2", size = 727050, upload-time = "2026-04-30T08:03:01.421Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
@@ -1313,7 +1583,8 @@ name = "tomlkit"
version = "0.13.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" }
wheels = [
@@ -1341,7 +1612,8 @@ name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version < '3.9'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
wheels = [
@@ -1364,6 +1636,35 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "uc-micro-py"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version == '3.9.*'",
"python_full_version >= '3.8.1' and python_full_version < '3.9'",
"python_full_version < '3.8.1'",
]
sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
]
[[package]]
name = "uc-micro-py"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.12' and python_full_version < '3.15'",
"python_full_version == '3.11.*'",
"python_full_version == '3.10.*'",
]
sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" },
]
[[package]]
name = "zipp"
version = "3.23.1"