From 5301b5384cfed63ffd6b6f9b16b7279a1c50d648 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:35:18 -0600 Subject: [PATCH 1/9] improve tmux --- workspaces.py | 268 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 212 insertions(+), 56 deletions(-) diff --git a/workspaces.py b/workspaces.py index 99b88ab..3dac0d0 100755 --- a/workspaces.py +++ b/workspaces.py @@ -27,7 +27,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from rich.console import Console from rich.table import Table from rich.prompt import Prompt, Confirm -from rich.panel import Panel from rich.syntax import Syntax from rich.panel import Panel from iterfzf import iterfzf @@ -71,7 +70,9 @@ class Settings(BaseSettings): ) @classmethod - def from_env_and_override(cls, override_workspaces_name: Optional[str]) -> "Settings": + def from_env_and_override( + cls, override_workspaces_name: Optional[str] + ) -> "Settings": """ Construct settings honoring: 1. CLI override @@ -209,15 +210,16 @@ def find_workspace_dir(workspaces_dir: Path, workspace_name: Optional[str]) -> P try: cwd.relative_to(workspaces_dir) except ValueError: - console.print( - f"[red]Not inside workspaces_dir ({workspaces_dir}). " - "Please use --workspace to specify a workspace.[/red]" - ) - raise typer.Exit(1) + workspace_root = pick_workspace_with_iterfzf(list_workspaces(workspaces_dir)) + if workspace_root is None: + console.print("[red]No workspace selected. Exiting.[/red]") + raise typer.Exit(code=1) + return workspace_root # The top-level workspace directory is the first component under workspaces_dir rel = cwd.relative_to(workspaces_dir) workspace_root = workspaces_dir / rel.parts[0] + print(f"Using workspace: {workspace_root}") return workspace_root @@ -238,7 +240,9 @@ def read_workspace_readme(ws_dir: Path) -> Tuple[str, str]: return ws_dir.name, "" # First non-empty line must be '# ...' per spec - first_non_empty_idx = next((i for i, line in enumerate(lines) if line.strip()), None) + first_non_empty_idx = next( + (i for i, line in enumerate(lines) if line.strip()), None + ) if first_non_empty_idx is None: return ws_dir.name, "" @@ -333,7 +337,7 @@ def main( # Default behavior when no subcommand is provided: if ctx.invoked_subcommand is None: - list_workspaces(ctx) + cli_list_workspaces(ctx) raise typer.Exit(0) @@ -345,8 +349,14 @@ def get_ctx_paths(ctx: typer.Context) -> Tuple[Settings, Path, Path]: # ---------------- list-workspaces ---------------- +def list_workspaces(workspaces_dir: Path): + workspaces_dir.mkdir(parents=True, exist_ok=True) + return sorted(p for p in workspaces_dir.iterdir() if p.is_dir()) + + @app.command("list") -def list_workspaces( +@app.command("ls", hidden=True) +def cli_list_workspaces( ctx: typer.Context, ): """ @@ -427,7 +437,9 @@ def create_workspace( dir_name = slugify_workspace_name(name) ws_dir = workspaces_dir / dir_name if ws_dir.exists(): - console.print(f"[red]Workspace dir '{dir_name}' already exists at {ws_dir}[/red]") + console.print( + f"[red]Workspace dir '{dir_name}' already exists at {ws_dir}[/red]" + ) raise typer.Exit(1) write_workspace_readme(ws_dir, name, description) @@ -464,7 +476,9 @@ def list_repos( ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, _desc = read_workspace_readme(ws_dir) @@ -520,6 +534,22 @@ def pick_repo_with_iterfzf(repos: List[Path]) -> Optional[Path]: return None +def pick_workspace_with_iterfzf(workspaces: List[Path]) -> Optional[Path]: + """ + Use iterfzf (Python library) to pick a workspace from a list of paths. + """ + names = [w.name for w in workspaces] + choice = iterfzf(names, prompt="pick a workspace> ") + + if not choice: + return None + + for w in workspaces: + if w.name == choice: + return w + return None + + @app.command("add-repo") @app.command("add", hidden=True) def add_repo( @@ -554,7 +584,9 @@ def add_repo( _settings, repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) # Use display title from README to derive branch name @@ -629,9 +661,7 @@ def add_repo( # ---------------- rm-workspace ---------------- -def find_repo_for_worktree( - worktree_path: Path, repos_dir: Path -) -> Optional[Path]: +def find_repo_for_worktree(worktree_path: Path, repos_dir: Path) -> Optional[Path]: """ Try to find the parent repo for a worktree, assuming it lives in repos_dir with the same directory name as the worktree. @@ -723,6 +753,7 @@ def is_branch_integrated_into_main( main_trees = {line.strip() for line in out.splitlines() if line.strip()} return branch_tree in main_trees + @app.command("rm") def remove_workspace( ctx: typer.Context, @@ -760,7 +791,9 @@ def remove_workspace( ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, _desc = read_workspace_readme(ws_dir) @@ -855,6 +888,10 @@ def remove_workspace( elif wt.exists(): # git worktree remove *should* delete the directory; if not, clean up. shutil.rmtree(wt) + + if in_tmux(): + session_name = f"{ws_dir.name}|{repo.name}" + remove_session_if_exists(session_name) # Finally, remove the workspace directory itself shutil.rmtree(ws_dir) console.print( @@ -873,9 +910,7 @@ def git_status_porcelain(repo_path: Path) -> List[Tuple[str, str]]: """ code, out, err = run_cmd(["git", "status", "--porcelain"], cwd=repo_path) if code != 0: - console.print( - f"[red]Failed to get status for {repo_path.name}:[/red]\n{err}" - ) + console.print(f"[red]Failed to get status for {repo_path.name}:[/red]\n{err}") return [] changes: List[Tuple[str, str]] = [] @@ -937,6 +972,7 @@ def choose_files_for_wip(repo_path: Path, changes: List[Tuple[str, str]]) -> Lis return selected_files + @app.command("wip") def wip_workspace( ctx: typer.Context, @@ -961,12 +997,15 @@ def wip_workspace( _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, _desc = read_workspace_readme(ws_dir) commit_message = f"wip: {title or ws_dir.name}" commit_workspace(ctx, workspace, message=commit_message) + @app.command("commit") def commit_workspace( ctx: typer.Context, @@ -996,13 +1035,14 @@ def commit_workspace( if not message: console.print( "[red]No commit message provided. Exiting.[/red]", - file=sys.stderr, ) raise typer.Exit(1) _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, _desc = read_workspace_readme(ws_dir) @@ -1034,9 +1074,7 @@ def commit_workspace( for f in files_to_stage: code, _out, err = run_cmd(["git", "add", f], cwd=wt) if code != 0: - console.print( - f"[red]Failed to add {f} in {wt.name}:[/red]\n{err}" - ) + console.print(f"[red]Failed to add {f} in {wt.name}:[/red]\n{err}") # Check if there is anything to commit code, out, _err = run_cmd(["git", "diff", "--cached", "--name-only"], cwd=wt) @@ -1047,13 +1085,9 @@ def commit_workspace( # Commit code, _out, err = run_cmd(["git", "commit", "-m", message], cwd=wt) if code != 0: - console.print( - f"[red]Failed to commit in {wt.name}:[/red]\n{err}" - ) + console.print(f"[red]Failed to commit in {wt.name}:[/red]\n{err}") else: - console.print( - f" [green]Created commit in {wt.name}:[/green] '{message}'" - ) + console.print(f" [green]Created commit in {wt.name}:[/green] '{message}'") # ---------------- push ---------------- @@ -1086,7 +1120,9 @@ def push_workspace( _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, _desc = read_workspace_readme(ws_dir) @@ -1122,9 +1158,7 @@ def push_workspace( f"[red]Failed to push {wt.name} ({branch}) to {remote}:[/red]\n{err}" ) else: - console.print( - f"[green]Pushed {wt.name} ({branch}) to {remote}[/green]" - ) + console.print(f"[green]Pushed {wt.name} ({branch}) to {remote}[/green]") # ---------------- status (current workspace) ---------------- @@ -1155,7 +1189,9 @@ def status_workspace( _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, desc = read_workspace_readme(ws_dir) @@ -1164,8 +1200,8 @@ def status_workspace( # console.print(Panel(desc, title=title)) table = Table( - title=f"Status for workspace:'{title}'\n(dir: {ws_dir.name})\n\n{desc}", - title_justify="left", + title=f"Status for workspace:'{title}'\n(dir: {ws_dir.name})\n\n{desc}", + title_justify="left", show_lines=False, ) table.add_column("Repo (dir)", style="bold") @@ -1211,6 +1247,7 @@ def status_workspace( if files_table.rows: console.print(files_table) + @app.command("diff") def diff_workspace( ctx: typer.Context, @@ -1238,7 +1275,9 @@ def diff_workspace( ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir.exists(): - console.print(f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]") + console.print( + f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" + ) raise typer.Exit(1) title, _desc = read_workspace_readme(ws_dir) @@ -1277,9 +1316,7 @@ def diff_workspace( code, out, err = run_cmd(cmd, cwd=wt) if code != 0: - console.print( - f"[red]Failed to get diff for {wt.name}:[/red]\n{err}" - ) + console.print(f"[red]Failed to get diff for {wt.name}:[/red]\n{err}") continue if not out.strip(): @@ -1300,12 +1337,20 @@ def diff_workspace( if not any_diffs: console.print("[green]No changes to diff in any repo.[/green]") + +def in_tmux() -> bool: + """Return True if inside tmux""" + return "TMUX" in os.environ + + def not_in_tmux() -> bool: - """Return True if not inside tmux or zellij.""" - return not os.environ.get("TMUX") and not os.environ.get("ZELLIJ") + """Return True if not inside tmux""" + return not in_tmux() -def run_tmux(args: list[str], *, clear_tmux_env: bool = False) -> subprocess.CompletedProcess: +def run_tmux( + args: list[str], *, clear_tmux_env: bool = False +) -> subprocess.CompletedProcess: """Run a tmux command.""" env = os.environ.copy() if clear_tmux_env: @@ -1314,9 +1359,16 @@ def run_tmux(args: list[str], *, clear_tmux_env: bool = False) -> subprocess.Com ["tmux", *args], env=env, check=False, + capture_output=True, ) +def get_tmux_sessions() -> List[str]: + """List all tmux sessions.""" + result = run_tmux(["list-sessions", "-F", "#{session_name}"]) + return result.stdout.decode().splitlines() + + def session_exists(session_name: str) -> bool: """Check if the tmux session exists.""" result = run_tmux(["has-session", "-t", f"={session_name}"]) @@ -1329,9 +1381,7 @@ def pick_project(base_dir: Path) -> Optional[str]: typer.echo(f"[ta] {base_dir} is not a directory.", err=True) return None - subdirs = sorted( - [p.name for p in base_dir.iterdir() if p.is_dir()] - ) + subdirs = sorted([p.name for p in base_dir.iterdir() if p.is_dir()]) if not subdirs: typer.echo(f"[ta] No subdirectories found in {base_dir}.", err=True) return None @@ -1459,14 +1509,28 @@ def attach_to_first_session() -> None: # After attach, show choose-tree (this will run in the attached session) run_tmux(["choose-tree", "-Za"]) + +def remove_session_if_exists(session_name: str) -> None: + r = run_tmux(["has-session", "-t", session_name]) + if r.returncode == 0: + remove_session(session_name) + + +def remove_session(session_name: str) -> None: + r = run_tmux(["kill-session", "-t", session_name]) + if r.returncode != 0: + raise typer.Exit(r.returncode) + + tmux_app = typer.Typer( name="tmux", help="tmux commands", add_completion=False, ) -@tmux_app.command('attach') -def attach( + +@tmux_app.command("attach") +def tmux_cli_attach( ctx: typer.Context, workspace: Optional[str] = typer.Option( None, @@ -1492,15 +1556,107 @@ def attach( if not repo: console.print("[red]No repo selected. Exiting.[/red]") raise typer.Exit(1) - print(ws_dir, repo) - session_name = f'{ws_dir.name}|{repo.name}' - print(f'{ws_dir.name}|{repo.name}') + session_name = f"ω|{ws_dir.name}|{repo.name}" + print(f"Session name: {session_name}") create_if_needed_and_attach(session_name, repo, False) +@tmux_app.command("list-sessions") +def tmux_cli_list_sessions( + ctx: typer.Context, + workspace: Optional[str] = typer.Option( + None, + "--workspace", + "-w", + help=( + "Workspace directory name to show status for. " + "If omitted, uses the workspace containing the current directory." + ), + ), +): + if not_in_tmux(): + console.print("[red]Not in tmux. Exiting.[/red]") + raise typer.Exit(1) + + if not workspace: + tmux_sessions = [session for session in get_tmux_sessions() if "|" in session] + console.print(tmux_sessions) + return + + tmux_sessions = [ + session for session in get_tmux_sessions() if session.startswith(workspace) + ] + console.print(tmux_sessions) + + +@tmux_app.command("remove") +def tmux_cli_remove( + ctx: typer.Context, + workspace: Optional[str] = typer.Option( + None, + "--workspace", + "-w", + help=( + "Workspace directory name to show status for. " + "If omitted, uses the workspace containing the current directory." + ), + ), + dry_run: bool = typer.Option( + False, + "--dry-run", + "-d", + help="Don't actually remove the session.", + ), +): + if not_in_tmux(): + console.print("[red]Not in tmux. Exiting.[/red]") + raise typer.Exit(1) + + if workspace: + tmux_sessions = [ + session for session in get_tmux_sessions() if session.startswith(workspace) + ] + else: + tmux_sessions = [ + session for session in get_tmux_sessions() if session.startswith("ω") + ] + if not dry_run: + if not Confirm.ask( + f"Are you sure you want to remove all tmux sessions?\n* {'\n* '.join(tmux_sessions)}\n", + default=False, + ): + raise typer.Exit(1) + + if dry_run: + console.print(tmux_sessions) + return + + for session_name in tmux_sessions: + remove_session(session_name) + + # + # remove_session_if_exists(session_name) + + app.add_typer(tmux_app) + +@app.command("attach") +def attach( + ctx: typer.Context, + workspace: Optional[str] = typer.Option( + None, + "--workspace", + "-w", + help=( + "Workspace directory name to show status for. " + "If omitted, uses the workspace containing the current directory." + ), + ), +): + if in_tmux(): + tmux_cli_attach(ctx, workspace) + + if __name__ == "__main__": app() - - -- 2.47.3 From 3019f0713e6765f17aac3c0baac1d2f5b5ae4e43 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:35:48 -0600 Subject: [PATCH 2/9] wip: feat: workspace tmux remove --- README.md | 0 justfile | 7 + pyproject.toml | 33 ++++ uv.lock | 481 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 521 insertions(+) create mode 100644 README.md create mode 100644 justfile create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/justfile b/justfile new file mode 100644 index 0000000..a5ef3c7 --- /dev/null +++ b/justfile @@ -0,0 +1,7 @@ + +format: + uv run ruff format . + +check: + uv run ruff check . + uv run ty check . diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7802c97 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,33 @@ +[project] +name = "workspaces" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +authors = [ + { name = "Waylon S. Walker", email = "waylon@waylonwalker.com" } +] +requires-python = ">=3.13" +dependencies = [ + "iterfzf>=1.8.0.62.0", + "pydantic>=2.12.5", + "pydantic-settings>=2.12.0", + "rich>=14.2.0", + "typer>=0.20.0", +] + +[project.scripts] +workspaces = "workspaces:main" + +[build-system] +requires = ["uv_build>=0.9.7,<0.10.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "ipython>=9.7.0", + "ruff>=0.14.6", + "ty>=0.0.1a28", +] + +[tool.ruff.lint.isort] +force-single-line = true diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..0ff264f --- /dev/null +++ b/uv.lock @@ -0,0 +1,481 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "executing" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, +] + +[[package]] +name = "ipython" +version = "9.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "iterfzf" +version = "1.8.0.62.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/3c/f1be6cbc6a805dfb1b21d0bdc8b850c3a18f55745acd7f3cf794e1454335/iterfzf-1.8.0.62.0.tar.gz", hash = "sha256:17f8b787da3561493608ce995f192421ccd5cc6be818fd12db5d3fc54b5aa5eb", size = 1827592, upload-time = "2025-05-15T13:13:10.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/07/b43cd0d821e39d2d9afc03ca15d658f9c7aed7d8363bb95b2f35c22500bf/iterfzf-1.8.0.62.0-py3-none-macosx_10_7_x86_64.macosx_10_9_x86_64.whl", hash = "sha256:057ac19b67128269a2c9efa7d419fef130d0df2210658d0e2fbe5a009b8b09e1", size = 1705552, upload-time = "2025-05-15T13:12:50.734Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/0c9125d0500df4cec54fcc76a8db86fbffee65fbfdcf85b712231a431829/iterfzf-1.8.0.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e18ea2ccf4d572695987d90537977ef060ebf4157e783bb1a72a7f3fa1da2cd0", size = 1618660, upload-time = "2025-05-15T13:12:52.924Z" }, + { url = "https://files.pythonhosted.org/packages/67/d5/16d76f04b23ecd4a2e98b112e73b10cfb058a4795a6e63d21df020d1c8cc/iterfzf-1.8.0.62.0-py3-none-manylinux_1_2_aarch64.manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24d5d658995be678a19c8ae633486daa9ef63fd17a2d759581c565885861817c", size = 1485885, upload-time = "2025-05-15T13:12:57.512Z" }, + { url = "https://files.pythonhosted.org/packages/df/37/4349484d7693324e40d51d0dc2f4d52b49b6765598e14af56642075ecdde/iterfzf-1.8.0.62.0-py3-none-manylinux_1_2_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15094a8ac72d0755d3657333fcd72329ef2d79ea6633c37a6be27be66671ae48", size = 1610003, upload-time = "2025-05-15T13:13:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/c9/85/520fedf1a01fe014366307ea4b1ce5527e29dfcf042155eb874972dcc1f7/iterfzf-1.8.0.62.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32878d993fc6058641bdec50867a25e199b40736cebb0e28449b8a61b054bf3d", size = 1482981, upload-time = "2025-05-15T13:13:02.153Z" }, + { url = "https://files.pythonhosted.org/packages/7b/08/e9fafe7bc4609f317d50df7cdb3c23a46ec65cedf255bd2ef55916d04e54/iterfzf-1.8.0.62.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b43e757b4c0141250aa48cb545083da25e41722d14b003fdc9bddb366a373479", size = 1604392, upload-time = "2025-05-15T13:13:04.102Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0f/7169c67ba591917c9eb09ab5433cb6c8cc2346baee7b9abab9351da277a6/iterfzf-1.8.0.62.0-py3-none-win_amd64.whl", hash = "sha256:b7cb95f93d5c1d901e8b7ccc886870f48f8331ebbba230cbd49eaedf045f142e", size = 1833659, upload-time = "2025-05-15T13:13:06.39Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/ff38147ebb94a9ad79781e7356f2b5e69bd79721a9f0653ccf25506c4cfe/iterfzf-1.8.0.62.0-py3-none-win_arm64.whl", hash = "sha256:9ae79840b14c090c6a4590add2eaa90f1e945319433d9c900badf0b588b576f8", size = 1689599, upload-time = "2025-05-15T13:13:08.463Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "matplotlib-inline" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, +] + +[[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 = "parso" +version = "0.8.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.52" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +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 = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f0/62b5a1a723fe183650109407fa56abb433b00aa1c0b9ba555f9c4efec2c6/ruff-0.14.6.tar.gz", hash = "sha256:6f0c742ca6a7783a736b867a263b9a7a80a45ce9bee391eeda296895f1b4e1cc", size = 5669501, upload-time = "2025-11-21T14:26:17.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/d2/7dd544116d107fffb24a0064d41a5d2ed1c9d6372d142f9ba108c8e39207/ruff-0.14.6-py3-none-linux_armv6l.whl", hash = "sha256:d724ac2f1c240dbd01a2ae98db5d1d9a5e1d9e96eba999d1c48e30062df578a3", size = 13326119, upload-time = "2025-11-21T14:25:24.2Z" }, + { url = "https://files.pythonhosted.org/packages/36/6a/ad66d0a3315d6327ed6b01f759d83df3c4d5f86c30462121024361137b6a/ruff-0.14.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9f7539ea257aa4d07b7ce87aed580e485c40143f2473ff2f2b75aee003186004", size = 13526007, upload-time = "2025-11-21T14:25:26.906Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9d/dae6db96df28e0a15dea8e986ee393af70fc97fd57669808728080529c37/ruff-0.14.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7f6007e55b90a2a7e93083ba48a9f23c3158c433591c33ee2e99a49b889c6332", size = 12676572, upload-time = "2025-11-21T14:25:29.826Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/f319e87759949062cfee1b26245048e92e2acce900ad3a909285f9db1859/ruff-0.14.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8e7b9d73d8728b68f632aa8e824ef041d068d231d8dbc7808532d3629a6bef", size = 13140745, upload-time = "2025-11-21T14:25:32.788Z" }, + { url = "https://files.pythonhosted.org/packages/95/d3/248c1efc71a0a8ed4e8e10b4b2266845d7dfc7a0ab64354afe049eaa1310/ruff-0.14.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d50d45d4553a3ebcbd33e7c5e0fe6ca4aafd9a9122492de357205c2c48f00775", size = 13076486, upload-time = "2025-11-21T14:25:35.601Z" }, + { url = "https://files.pythonhosted.org/packages/a5/19/b68d4563fe50eba4b8c92aa842149bb56dd24d198389c0ed12e7faff4f7d/ruff-0.14.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:118548dd121f8a21bfa8ab2c5b80e5b4aed67ead4b7567790962554f38e598ce", size = 13727563, upload-time = "2025-11-21T14:25:38.514Z" }, + { url = "https://files.pythonhosted.org/packages/47/ac/943169436832d4b0e867235abbdb57ce3a82367b47e0280fa7b4eabb7593/ruff-0.14.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:57256efafbfefcb8748df9d1d766062f62b20150691021f8ab79e2d919f7c11f", size = 15199755, upload-time = "2025-11-21T14:25:41.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b9/288bb2399860a36d4bb0541cb66cce3c0f4156aaff009dc8499be0c24bf2/ruff-0.14.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ff18134841e5c68f8e5df1999a64429a02d5549036b394fafbe410f886e1989d", size = 14850608, upload-time = "2025-11-21T14:25:44.428Z" }, + { url = "https://files.pythonhosted.org/packages/ee/b1/a0d549dd4364e240f37e7d2907e97ee80587480d98c7799d2d8dc7a2f605/ruff-0.14.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29c4b7ec1e66a105d5c27bd57fa93203637d66a26d10ca9809dc7fc18ec58440", size = 14118754, upload-time = "2025-11-21T14:25:47.214Z" }, + { url = "https://files.pythonhosted.org/packages/13/ac/9b9fe63716af8bdfddfacd0882bc1586f29985d3b988b3c62ddce2e202c3/ruff-0.14.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:167843a6f78680746d7e226f255d920aeed5e4ad9c03258094a2d49d3028b105", size = 13949214, upload-time = "2025-11-21T14:25:50.002Z" }, + { url = "https://files.pythonhosted.org/packages/12/27/4dad6c6a77fede9560b7df6802b1b697e97e49ceabe1f12baf3ea20862e9/ruff-0.14.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:16a33af621c9c523b1ae006b1b99b159bf5ac7e4b1f20b85b2572455018e0821", size = 14106112, upload-time = "2025-11-21T14:25:52.841Z" }, + { url = "https://files.pythonhosted.org/packages/6a/db/23e322d7177873eaedea59a7932ca5084ec5b7e20cb30f341ab594130a71/ruff-0.14.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1432ab6e1ae2dc565a7eea707d3b03a0c234ef401482a6f1621bc1f427c2ff55", size = 13035010, upload-time = "2025-11-21T14:25:55.536Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/20e21d4d69dbb35e6a1df7691e02f363423658a20a2afacf2a2c011800dc/ruff-0.14.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c55cfbbe7abb61eb914bfd20683d14cdfb38a6d56c6c66efa55ec6570ee4e71", size = 13054082, upload-time = "2025-11-21T14:25:58.625Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/906ee6a0464c3125c8d673c589771a974965c2be1a1e28b5c3b96cb6ef88/ruff-0.14.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:efea3c0f21901a685fff4befda6d61a1bf4cb43de16da87e8226a281d614350b", size = 13303354, upload-time = "2025-11-21T14:26:01.816Z" }, + { url = "https://files.pythonhosted.org/packages/4c/58/60577569e198d56922b7ead07b465f559002b7b11d53f40937e95067ca1c/ruff-0.14.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:344d97172576d75dc6afc0e9243376dbe1668559c72de1864439c4fc95f78185", size = 14054487, upload-time = "2025-11-21T14:26:05.058Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/8e4e0639e4cc12547f41cb771b0b44ec8225b6b6a93393176d75fe6f7d40/ruff-0.14.6-py3-none-win32.whl", hash = "sha256:00169c0c8b85396516fdd9ce3446c7ca20c2a8f90a77aa945ba6b8f2bfe99e85", size = 13013361, upload-time = "2025-11-21T14:26:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/fb/02/82240553b77fd1341f80ebb3eaae43ba011c7a91b4224a9f317d8e6591af/ruff-0.14.6-py3-none-win_amd64.whl", hash = "sha256:390e6480c5e3659f8a4c8d6a0373027820419ac14fa0d2713bd8e6c3e125b8b9", size = 14432087, upload-time = "2025-11-21T14:26:10.891Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1f/93f9b0fad9470e4c829a5bb678da4012f0c710d09331b860ee555216f4ea/ruff-0.14.6-py3-none-win_arm64.whl", hash = "sha256:d43c81fbeae52cfa8728d8766bbf46ee4298c888072105815b392da70ca836b2", size = 13520930, upload-time = "2025-11-21T14:26:13.951Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "ty" +version = "0.0.1a28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/8b/8a87df1d93ad4e2e88f08f94941b9f9479ccb323100fb52253cecbde8978/ty-0.0.1a28.tar.gz", hash = "sha256:6454f2bc0d5b716aeaba3e32c4585a14a0d6bfc7e90d5aba64539fa33df824c4", size = 4584440, upload-time = "2025-11-26T00:27:09.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/7a/768f3d9945066a9a44f9ed280e4717409b772fca1ef165e112827abf2ee6/ty-0.0.1a28-py3-none-linux_armv6l.whl", hash = "sha256:0ea28aaaf35176a75ce85da7a4b7f577f3a3319a1eb4d13c0105629e239a7d95", size = 9500811, upload-time = "2025-11-26T00:27:26.134Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cc/d6e4e433bd91043d1eb2ecc7908000585100a5cbdd548d85082e1e07865d/ty-0.0.1a28-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:463f8b6bee5c3d338a535c40764a4f209f5465caecbc9f7358ee2a7f8b2d321e", size = 9286280, upload-time = "2025-11-26T00:27:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/77/68/00e8e7f280fbef2e89df10e6c9ce896dd6716bffc2e8e7ece58503b767e5/ty-0.0.1a28-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7d037ea9f896e6e9b96ca066959e2a7600db0da9e4038f1247c9337af253cc8c", size = 8810453, upload-time = "2025-11-26T00:27:07.812Z" }, + { url = "https://files.pythonhosted.org/packages/10/1b/ef72e26f487272b60156e0f527a5fbc27da799accad3420d01bc08101ca8/ty-0.0.1a28-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad5099ffaa891391733d6fd85bcdd00ad68042a2da4f80a114b9e7044e6f7460", size = 9098344, upload-time = "2025-11-26T00:27:22.531Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/e56c5623c604d20fa26d320a73bc4fb7c2db28e14ba021409c767c4ddfdf/ty-0.0.1a28-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:587652aecb8d238adcb45ae7cd12efd27b9778f74b636cbbe5dcc2e938f9af4e", size = 9303714, upload-time = "2025-11-26T00:26:57.946Z" }, + { url = "https://files.pythonhosted.org/packages/eb/04/61518d3eac0357305e3a06c9a4cedbb49bc9f343d38ba26194c15a81f22e/ty-0.0.1a28-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d9556c87419264ffc3071a249f89d890a29df5d09abd8d216bac850ad2d7ba9", size = 9668395, upload-time = "2025-11-26T00:27:12.893Z" }, + { url = "https://files.pythonhosted.org/packages/fd/01/ef22fc8e3d9415d2ab2def0f562fe6ee7ae28b99dc180acd636486a9f818/ty-0.0.1a28-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7481abc03a0aabf966c9e1cccb18c9edbb7cf01ec011568cd24feb1ab45faef7", size = 10269943, upload-time = "2025-11-26T00:27:02.018Z" }, + { url = "https://files.pythonhosted.org/packages/16/f7/bb94f55c6f3bfc3da543e6b1ec32877e107b2afb8cae3057ae9f5a8f4eaa/ty-0.0.1a28-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fd4926f668b733aeadd09f7d16e63af30cba5438bbba1274f950a1059c8d64", size = 10023310, upload-time = "2025-11-26T00:27:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/9a/58/ebaefa1b27b4aea8156f1b43d6d431afd8061e76e1c96e83dad8a0dcb555/ty-0.0.1a28-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fb119d7db1a064dd74ccedf78bdc5caae30cf5de421dff972a849bcff411269", size = 10034408, upload-time = "2025-11-26T00:27:18.561Z" }, + { url = "https://files.pythonhosted.org/packages/da/66/97be24c8abbcd803dab65cd2b430330e449e4542c0e0396e15fe32f4e2c2/ty-0.0.1a28-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd7f7d744920af9ceaf7fe6db290366abefbcffd7cce54f15e8cef6a86e2df31", size = 9597359, upload-time = "2025-11-26T00:27:03.803Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c8/a7451f1ca4d8ed12c025a5c306e9527bd9269abacdf2b2b8d0ca8bb90a13/ty-0.0.1a28-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c20c6cf7e786ecf6c8f34892240b4b1ae8b1adce52243868aa400c80b7a9bc1d", size = 9069439, upload-time = "2025-11-26T00:27:14.768Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b9/d212887e13f3db925287f6be5addaf37190070956c960c73e22f93509273/ty-0.0.1a28-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:54c94a06c0236dfd249217e28816b6aedfc40e71d5b5131924efa3b095dfcf1a", size = 9332037, upload-time = "2025-11-26T00:27:00.138Z" }, + { url = "https://files.pythonhosted.org/packages/1d/14/3dc72136a72d354cdc93b509c35f4a426869879fa9e0346f1cd7d2bba3f7/ty-0.0.1a28-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1a15eb2535229ab65aaafbe3fb22c3d289c4e34cda92fb748815573b6d52fe3a", size = 9428504, upload-time = "2025-11-26T00:27:16.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/65/e15984e245fe330dfdc665cc7c492c633149ff97b3f95af32bdd08b74fdb/ty-0.0.1a28-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6c2ebd5314707cd26aabe77b1d664e597b7b29a8d07fed5091f986ebdaa261a9", size = 9720869, upload-time = "2025-11-26T00:27:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/a5/91/5826e5f78fc5ee685b34a1904cb5da8b3ab83d4c04e5574c4542728c2422/ty-0.0.1a28-py3-none-win32.whl", hash = "sha256:ae10abd8575d28744d905979632040222581ba364281abf75baf8f269a10ffc3", size = 8950581, upload-time = "2025-11-26T00:27:24.346Z" }, + { url = "https://files.pythonhosted.org/packages/4f/5e/6380d565dfb286634facbe71fb389dc9a8d4379f18d55a6feac392bd5755/ty-0.0.1a28-py3-none-win_amd64.whl", hash = "sha256:44ef82c1169c050ad9e91b2d76251be097ddd163719735cf7e5a978065f6b87c", size = 9789598, upload-time = "2025-11-26T00:27:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/55/48/fec040641bd4c9599fecc0bb74e697c79ea3fa234b25b04b68823aca55a5/ty-0.0.1a28-py3-none-win_arm64.whl", hash = "sha256:051c1d43df50366fb8e795ae52af8f2015b79d176dbb82cdd45668074847ddf3", size = 9278405, upload-time = "2025-11-26T00:27:11.066Z" }, +] + +[[package]] +name = "typer" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492, upload-time = "2025-10-20T17:03:49.445Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028, upload-time = "2025-10-20T17:03:47.617Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +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 = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, +] + +[[package]] +name = "workspaces" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "iterfzf" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ipython" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "iterfzf", specifier = ">=1.8.0.62.0" }, + { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pydantic-settings", specifier = ">=2.12.0" }, + { name = "rich", specifier = ">=14.2.0" }, + { name = "typer", specifier = ">=0.20.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "ipython", specifier = ">=9.7.0" }, + { name = "ruff", specifier = ">=0.14.6" }, + { name = "ty", specifier = ">=0.0.1a28" }, +] -- 2.47.3 From e2d11a7ea42ce7a6143889096f7c167b0779f245 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:37:30 -0600 Subject: [PATCH 3/9] wip: feat: workspace tmux remove --- src/workspaces/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/workspaces/__init__.py diff --git a/src/workspaces/__init__.py b/src/workspaces/__init__.py new file mode 100644 index 0000000..e69de29 -- 2.47.3 From 7ec81ffdc73f1a9f09c1d856ec7f79959960d7e3 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:50:01 -0600 Subject: [PATCH 4/9] wip: feat: workspace tmux remove --- justfile | 2 +- pyproject.toml | 63 ++++++++++++++++++++++++++++++ workspaces.py | 104 +++++++++++++++++++++++++------------------------ 3 files changed, 118 insertions(+), 51 deletions(-) diff --git a/justfile b/justfile index a5ef3c7..77f84ef 100644 --- a/justfile +++ b/justfile @@ -3,5 +3,5 @@ format: uv run ruff format . check: - uv run ruff check . + uv run ruff check . --fix uv run ty check . diff --git a/pyproject.toml b/pyproject.toml index 7802c97..e453d48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,3 +31,66 @@ dev = [ [tool.ruff.lint.isort] force-single-line = true + +[tool.ruff] +target-version = "py312" + +[tool.ruff.lint] +ignore = [ +"E501", +"COM812", # flake8-commas +] +select = [ +"F", # Pyflakes +"E", # Error +"W", # Warning +# "C90", # mccabe +"I", # isort +"N", # pep8-naming +# "D", # pydocstyle +# "UP", # pyupgrade +"YTT", # flake8-2020 +# "ANN", # flake8-annotations +# "S", # flake8-bandit +# "BLE", # flake8-blind-except +# "FBT", # flake8-boolean-trap +"B", # flake8-bugbear +"A", # flake8-builtins +"COM", # flake8-commas +"C4", # flake8-comprehensions +"DTZ", # flake8-datetimez +"T10", # flake8-debugger +"DJ", # flake8-django +"EM", # flake8-errmsg +"EXE", # flake8-executable +"ISC", # flake8-implicit-str-concat +"ICN", # flake8-import-conventions +"G", # flake8-logging-format +# "INP", # flake8-no-pep420 +"PIE", # flake8-pie +"T20", # flake8-print +"PYI", # flake8-pyi +"PT", # flake8-pytest-style +"Q", # flake8-quotes +"RSE", # flake8-raise +"RET", # flake8-return +# "SLF", # flake8-self +# "SIM", # flake8-simplify +"TID", # flake8-tidy-imports +"TCH", # flake8-type-checking +# "INT", # flake8-gettext +# "ARG", # flake8-unused-arguments +"PTH", # flake8-use-pathlib +# "ERA", # eradicate +"PD", # pandas-vet +"PGH", # pygrep-hooks +# "PL", # Pylint +# "PLC", # Convention +"PLE", # Error +# "PLR", # Refactor +"PLW", # Warning +# "TRY", # tryceratops +"NPY", # NumPy-specific rules +# "RUF", # Ruff-specific rules +] + diff --git a/workspaces.py b/workspaces.py index 3dac0d0..33dcdeb 100755 --- a/workspaces.py +++ b/workspaces.py @@ -11,25 +11,28 @@ # /// from __future__ import annotations + import os import re import shutil import subprocess from dataclasses import dataclass from pathlib import Path -from typing import List, Optional, Tuple - +from typing import List +from typing import Optional +from typing import Tuple import typer - -from pydantic import Field -from pydantic_settings import BaseSettings, SettingsConfigDict -from rich.console import Console -from rich.table import Table -from rich.prompt import Prompt, Confirm -from rich.syntax import Syntax -from rich.panel import Panel from iterfzf import iterfzf +from pydantic import Field +from pydantic_settings import BaseSettings +from pydantic_settings import SettingsConfigDict +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Confirm +from rich.prompt import Prompt +from rich.syntax import Syntax +from rich.table import Table app = typer.Typer( help="Workspace management tool", @@ -94,8 +97,8 @@ def resolve_paths(workspaces_name: Optional[str]) -> Tuple[Settings, Path, Path] """ base_settings = Settings.from_env_and_override(workspaces_name) name = base_settings.workspaces_name - repos_dir = Path(os.path.expanduser(f"~/{name}")).resolve() - workspaces_dir = Path(os.path.expanduser(f"~/{name}.workspaces")).resolve() + repos_dir = Path(f"~/{name}").expanduser().resolve() + workspaces_dir = Path(f"~/{name}.workspaces").expanduser().resolve() return base_settings, repos_dir, workspaces_dir @@ -209,17 +212,17 @@ def find_workspace_dir(workspaces_dir: Path, workspace_name: Optional[str]) -> P cwd = Path.cwd().resolve() try: cwd.relative_to(workspaces_dir) - except ValueError: + except ValueError as e: workspace_root = pick_workspace_with_iterfzf(list_workspaces(workspaces_dir)) if workspace_root is None: console.print("[red]No workspace selected. Exiting.[/red]") - raise typer.Exit(code=1) + raise typer.Exit(code=1) from e return workspace_root # The top-level workspace directory is the first component under workspaces_dir rel = cwd.relative_to(workspaces_dir) workspace_root = workspaces_dir / rel.parts[0] - print(f"Using workspace: {workspace_root}") + console.print(f"Using workspace: {workspace_root}") return workspace_root @@ -1394,6 +1397,7 @@ def pick_project(base_dir: Path) -> Optional[str]: ["fzf", "--reverse", f"--header=Select project from {base_dir.name} >"], input="\n".join(subdirs), text=True, + check=False, capture_output=True, ) @@ -1423,41 +1427,40 @@ def create_detached_session( clear_tmux_env=True, ) return r.returncode == 0 - else: - r = run_tmux( - ["new-session", "-Ad", "-s", session_name, "-c", str(path_name)], - clear_tmux_env=True, - ) - if r.returncode != 0: - return False + r = run_tmux( + ["new-session", "-Ad", "-s", session_name, "-c", str(path_name)], + clear_tmux_env=True, + ) + if r.returncode != 0: + return False - r = run_tmux( - [ - "split-window", - "-vb", - "-t", - session_name, - "-c", - str(path_name), - "-p", - "70", - ], - clear_tmux_env=True, - ) - if r.returncode != 0: - return False + r = run_tmux( + [ + "split-window", + "-vb", + "-t", + session_name, + "-c", + str(path_name), + "-p", + "70", + ], + clear_tmux_env=True, + ) + if r.returncode != 0: + return False - r = run_tmux( - [ - "send-keys", - "-t", - session_name, - "nvim '+Telescope find_files'", - "Enter", - ], - clear_tmux_env=True, - ) - return r.returncode == 0 + r = run_tmux( + [ + "send-keys", + "-t", + session_name, + "nvim '+Telescope find_files'", + "Enter", + ], + clear_tmux_env=True, + ) + return r.returncode == 0 def create_if_needed_and_attach( @@ -1494,6 +1497,7 @@ def attach_to_first_session() -> None: list_proc = subprocess.run( ["tmux", "list-sessions", "-F", "#{session_name}"], text=True, + check=False, capture_output=True, ) if list_proc.returncode != 0 or not list_proc.stdout.strip(): @@ -1552,12 +1556,12 @@ def tmux_cli_attach( console.print(f"[red]Workspace '{workspace}' does not exist at {ws_dir}[/red]") raise typer.Exit(1) # pick repo in workspace - repo = pick_repo_with_iterfzf([dir for dir in ws_dir.iterdir() if dir.is_dir()]) + repo = pick_repo_with_iterfzf([directory for directory in ws_dir.iterdir() if directory.is_dir()]) if not repo: console.print("[red]No repo selected. Exiting.[/red]") raise typer.Exit(1) session_name = f"ω|{ws_dir.name}|{repo.name}" - print(f"Session name: {session_name}") + console.print(f"Session name: {session_name}") create_if_needed_and_attach(session_name, repo, False) -- 2.47.3 From 0bdfb253efed09172f7561bb642c9606fbc4b779 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:54:17 -0600 Subject: [PATCH 5/9] wip: feat: workspace tmux remove --- pyproject.toml | 8 ++++---- workspaces.py | 10 ++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e453d48..226e898 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,16 +81,16 @@ select = [ # "INT", # flake8-gettext # "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib -# "ERA", # eradicate + "ERA", # eradicate "PD", # pandas-vet "PGH", # pygrep-hooks # "PL", # Pylint -# "PLC", # Convention + "PLC", # Convention "PLE", # Error # "PLR", # Refactor "PLW", # Warning -# "TRY", # tryceratops + "TRY", # tryceratops "NPY", # NumPy-specific rules -# "RUF", # Ruff-specific rules + "RUF", # Ruff-specific rules ] diff --git a/workspaces.py b/workspaces.py index 33dcdeb..e948a82 100755 --- a/workspaces.py +++ b/workspaces.py @@ -158,7 +158,8 @@ def get_git_status(repo_path: Path) -> GitStatus: for line in out.splitlines(): if line.startswith("# branch.ab"): - # Example: "# branch.ab +1 -2" + # Example: + # branch.ab +1 -2 m = re.search(r"\+(\d+)\s+-(\d+)", line) if m: ahead = int(m.group(1)) @@ -920,7 +921,6 @@ def git_status_porcelain(repo_path: Path) -> List[Tuple[str, str]]: for line in out.splitlines(): if not line.strip(): continue - # Format: 'XY path' status = line[:2] path = line[3:] changes.append((status, path)) @@ -1047,7 +1047,6 @@ def commit_workspace( f"[red]Workspace '{ws_dir.name}' does not exist at {ws_dir}[/red]" ) raise typer.Exit(1) - title, _desc = read_workspace_readme(ws_dir) worktrees: List[Path] = [] for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()): @@ -1199,8 +1198,6 @@ def status_workspace( title, desc = read_workspace_readme(ws_dir) - # if desc: - # console.print(Panel(desc, title=title)) table = Table( title=f"Status for workspace:'{title}'\n(dir: {ws_dir.name})\n\n{desc}", @@ -1549,7 +1546,6 @@ def tmux_cli_attach( """ Attach or create a session for a repo in a workspace. """ - # ws_dir = get_workspace_dir(workspace) _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) if not ws_dir: @@ -1638,8 +1634,6 @@ def tmux_cli_remove( for session_name in tmux_sessions: remove_session(session_name) - # - # remove_session_if_exists(session_name) app.add_typer(tmux_app) -- 2.47.3 From 7b1a33414aa69ea7d2e6dd505feaf971299f86cd Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:55:28 -0600 Subject: [PATCH 6/9] wip: feat: workspace tmux remove --- pyproject.toml | 2 +- workspaces.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 226e898..562fefa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ select = [ "TID", # flake8-tidy-imports "TCH", # flake8-type-checking # "INT", # flake8-gettext -# "ARG", # flake8-unused-arguments + "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib "ERA", # eradicate "PD", # pandas-vet diff --git a/workspaces.py b/workspaces.py index e948a82..90072d5 100755 --- a/workspaces.py +++ b/workspaces.py @@ -676,7 +676,7 @@ def find_repo_for_worktree(worktree_path: Path, repos_dir: Path) -> Optional[Pat return None -def has_unpushed_commits(repo_path: Path, branch: str) -> bool: +def has_unpushed_commits(repo_path: Path) -> bool: """ Detect if branch has commits not on its upstream. Uses 'git rev-list @{u}..HEAD'; if non-empty, there are unpushed commits. @@ -839,7 +839,7 @@ def remove_workspace( # Only care about "unpushed commits" if the branch is NOT integrated into main. if repo is not None and branch != "?" and not integrated: - if has_unpushed_commits(wt, branch): + if has_unpushed_commits(wt): problems.append(f"{wt.name}: unpushed commits on '{branch}'") if problems and not force: console.print( @@ -1563,7 +1563,6 @@ def tmux_cli_attach( @tmux_app.command("list-sessions") def tmux_cli_list_sessions( - ctx: typer.Context, workspace: Optional[str] = typer.Option( None, "--workspace", @@ -1591,7 +1590,6 @@ def tmux_cli_list_sessions( @tmux_app.command("remove") def tmux_cli_remove( - ctx: typer.Context, workspace: Optional[str] = typer.Option( None, "--workspace", -- 2.47.3 From f51701818a1905493a646d3bcba927aceefdccc1 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:58:19 -0600 Subject: [PATCH 7/9] wip: feat: workspace tmux remove --- pyproject.toml | 4 ++-- workspaces.py | 22 +++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 562fefa..1dffd73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,10 +75,10 @@ select = [ "RSE", # flake8-raise "RET", # flake8-return # "SLF", # flake8-self -# "SIM", # flake8-simplify + "SIM", # flake8-simplify "TID", # flake8-tidy-imports "TCH", # flake8-type-checking -# "INT", # flake8-gettext + "INT", # flake8-gettext "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib "ERA", # eradicate diff --git a/workspaces.py b/workspaces.py index 90072d5..5ca4055 100755 --- a/workspaces.py +++ b/workspaces.py @@ -838,9 +838,8 @@ def remove_workspace( problems.append(f"{wt.name}: dirty working tree on '{branch}'") # Only care about "unpushed commits" if the branch is NOT integrated into main. - if repo is not None and branch != "?" and not integrated: - if has_unpushed_commits(wt): - problems.append(f"{wt.name}: unpushed commits on '{branch}'") + if repo is not None and branch != "?" and not integrated and has_unpushed_commits(wt): + problems.append(f"{wt.name}: unpushed commits on '{branch}'") if problems and not force: console.print( "[red]Refusing to remove workspace; found dirty or unpushed work:[/red]" @@ -968,12 +967,7 @@ def choose_files_for_wip(repo_path: Path, changes: List[Tuple[str, str]]) -> Lis return [] # iterfzf may return a single string or list depending on version; # normalize to list of strings. - if isinstance(selected, str): - selected_files = [selected] - else: - selected_files = list(selected) - - return selected_files + return [selected] if isinstance(selected, str) else list(selected) @app.command("wip") @@ -1480,9 +1474,8 @@ def create_if_needed_and_attach( return r.returncode == 0 # Inside tmux - if not session_exists(session_name): - if not create_detached_session(session_name, path_name, start_mode): - return False + if not session_exists(session_name) and not create_detached_session(session_name, path_name, start_mode): + return False r = run_tmux(["switch-client", "-t", session_name]) return r.returncode == 0 @@ -1618,12 +1611,11 @@ def tmux_cli_remove( tmux_sessions = [ session for session in get_tmux_sessions() if session.startswith("ω") ] - if not dry_run: - if not Confirm.ask( + if not dry_run and not Confirm.ask( f"Are you sure you want to remove all tmux sessions?\n* {'\n* '.join(tmux_sessions)}\n", default=False, ): - raise typer.Exit(1) + raise typer.Exit(1) if dry_run: console.print(tmux_sessions) -- 2.47.3 From a0d308c19f2c0c6a6d8805ffde0cb43303f1195f Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:58:50 -0600 Subject: [PATCH 8/9] wip: feat: workspace tmux remove --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1dffd73..13b744e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ select = [ "ISC", # flake8-implicit-str-concat "ICN", # flake8-import-conventions "G", # flake8-logging-format -# "INP", # flake8-no-pep420 + "INP", # flake8-no-pep420 "PIE", # flake8-pie "T20", # flake8-print "PYI", # flake8-pyi @@ -74,7 +74,7 @@ select = [ "Q", # flake8-quotes "RSE", # flake8-raise "RET", # flake8-return -# "SLF", # flake8-self + "SLF", # flake8-self "SIM", # flake8-simplify "TID", # flake8-tidy-imports "TCH", # flake8-type-checking -- 2.47.3 From 7bb9b49e9b7fbd96846e5396e7da9e71aae2b392 Mon Sep 17 00:00:00 2001 From: "Waylon S. Walker" Date: Wed, 26 Nov 2025 10:59:52 -0600 Subject: [PATCH 9/9] wip: feat: workspace tmux remove --- pyproject.toml | 2 +- workspaces.py | 182 ++++++++++++++++++++----------------------------- 2 files changed, 75 insertions(+), 109 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 13b744e..bb1c52b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ select = [ "I", # isort "N", # pep8-naming # "D", # pydocstyle -# "UP", # pyupgrade + "UP", # pyupgrade "YTT", # flake8-2020 # "ANN", # flake8-annotations # "S", # flake8-bandit diff --git a/workspaces.py b/workspaces.py index 5ca4055..2a332b8 100755 --- a/workspaces.py +++ b/workspaces.py @@ -18,9 +18,6 @@ import shutil import subprocess from dataclasses import dataclass from pathlib import Path -from typing import List -from typing import Optional -from typing import Tuple import typer from iterfzf import iterfzf @@ -47,8 +44,7 @@ console = Console() class Settings(BaseSettings): - """ - Global configuration for workspaces. + """Global configuration for workspaces. Resolution for workspaces_name: 1. Command-line flag --workspaces-name @@ -74,10 +70,9 @@ class Settings(BaseSettings): @classmethod def from_env_and_override( - cls, override_workspaces_name: Optional[str] - ) -> "Settings": - """ - Construct settings honoring: + cls, override_workspaces_name: str | None + ) -> Settings: + """Construct settings honoring: 1. CLI override 2. WORKSPACES_NAME env 3. default "git" @@ -91,9 +86,8 @@ class Settings(BaseSettings): return s -def resolve_paths(workspaces_name: Optional[str]) -> Tuple[Settings, Path, Path]: - """ - Build Settings and derived paths, honoring CLI override of workspaces_name. +def resolve_paths(workspaces_name: str | None) -> tuple[Settings, Path, Path]: + """Build Settings and derived paths, honoring CLI override of workspaces_name. """ base_settings = Settings.from_env_and_override(workspaces_name) name = base_settings.workspaces_name @@ -115,15 +109,14 @@ class GitStatus: @property def indicator(self) -> str: - """ - Build ASCII indicator: + """Build ASCII indicator: - clean: "·" - ahead 1: "↑1" - behind 2: "↓2" - both ahead/behind: "↑1 ↓2" - add '*' when dirty, e.g. "↑1*" or "↑1 ↓2*" """ - parts: List[str] = [] + parts: list[str] = [] if self.ahead: parts.append(f"↑{self.ahead}") if self.behind: @@ -135,8 +128,7 @@ class GitStatus: def get_git_status(repo_path: Path) -> GitStatus: - """ - Get ahead/behind and dirty info for a Git repo. + """Get ahead/behind and dirty info for a Git repo. Uses `git status --porcelain=v2 --branch` and parses: - '# branch.ab +A -B' for ahead/behind @@ -170,7 +162,7 @@ def get_git_status(repo_path: Path) -> GitStatus: return GitStatus(ahead=ahead, behind=behind, dirty=dirty) -def get_current_branch(repo_path: Path) -> Optional[str]: +def get_current_branch(repo_path: Path) -> str | None: try: out = subprocess.check_output( ["git", "rev-parse", "--abbrev-ref", "HEAD"], @@ -188,7 +180,7 @@ def ensure_git_repo(path: Path) -> bool: return (path / ".git").exists() -def run_cmd(cmd: List[str], cwd: Optional[Path] = None) -> Tuple[int, str, str]: +def run_cmd(cmd: list[str], cwd: Path | None = None) -> tuple[int, str, str]: proc = subprocess.Popen( cmd, cwd=cwd, @@ -200,9 +192,8 @@ def run_cmd(cmd: List[str], cwd: Optional[Path] = None) -> Tuple[int, str, str]: return proc.returncode, out, err -def find_workspace_dir(workspaces_dir: Path, workspace_name: Optional[str]) -> Path: - """ - Resolve the directory of a workspace. +def find_workspace_dir(workspaces_dir: Path, workspace_name: str | None) -> Path: + """Resolve the directory of a workspace. - If workspace_name given: use workspaces_dir / workspace_name - Else: use current working directory (must be inside workspaces_dir). @@ -227,9 +218,8 @@ def find_workspace_dir(workspaces_dir: Path, workspace_name: Optional[str]) -> P return workspace_root -def read_workspace_readme(ws_dir: Path) -> Tuple[str, str]: - """ - Return (name_from_h1, description_from_rest_of_file). +def read_workspace_readme(ws_dir: Path) -> tuple[str, str]: + """Return (name_from_h1, description_from_rest_of_file). If file missing or malformed, fallback appropriately. """ readme = ws_dir / "readme.md" @@ -270,8 +260,7 @@ def write_workspace_readme(ws_dir: Path, name: str, description: str) -> None: def slugify_workspace_name(name: str) -> str: - """ - Turn arbitrary workspace name into a safe directory/worktree name. + """Turn arbitrary workspace name into a safe directory/worktree name. - Lowercase - Replace ':' with '-' FIRST (special case so 'fix:my issue' -> 'fix/my-issue' branch later) @@ -294,8 +283,7 @@ def slugify_workspace_name(name: str) -> str: def branch_name_for_workspace(name: str) -> str: - """ - Compute branch name from workspace name. + """Compute branch name from workspace name. Rules: - Start from slugified name (no spaces/specials). @@ -317,7 +305,7 @@ def branch_name_for_workspace(name: str) -> str: @app.callback() def main( ctx: typer.Context, - workspaces_name: Optional[str] = typer.Option( + workspaces_name: str | None = typer.Option( None, "--workspaces-name", "-W", @@ -327,8 +315,7 @@ def main( ), ), ): - """ - Manage workspaces and associated Git worktrees. + """Manage workspaces and associated Git worktrees. If no command is given, this will list workspaces. """ @@ -345,7 +332,7 @@ def main( raise typer.Exit(0) -def get_ctx_paths(ctx: typer.Context) -> Tuple[Settings, Path, Path]: +def get_ctx_paths(ctx: typer.Context) -> tuple[Settings, Path, Path]: obj = ctx.obj or {} return obj["settings"], obj["repos_dir"], obj["workspaces_dir"] @@ -363,8 +350,7 @@ def list_workspaces(workspaces_dir: Path): def cli_list_workspaces( ctx: typer.Context, ): - """ - List all workspaces. + """List all workspaces. Shows: - workspace directory name @@ -386,7 +372,7 @@ def cli_list_workspaces( for ws in sorted(p for p in workspaces_dir.iterdir() if p.is_dir()): title, desc = read_workspace_readme(ws) - repos: List[str] = [] + repos: list[str] = [] for child in sorted(p for p in ws.iterdir() if p.is_dir()): if child.name.lower() == "readme.md": continue @@ -409,21 +395,20 @@ def cli_list_workspaces( @app.command("new", hidden=True) def create_workspace( ctx: typer.Context, - name: Optional[str] = typer.Option( + name: str | None = typer.Option( None, "--name", "-n", help="Name of the new workspace (display name; can contain spaces).", ), - description: Optional[str] = typer.Option( + description: str | None = typer.Option( None, "--description", "-d", help="Description of the workspace. Will be written into readme.md.", ), ): - """ - Create a new workspace. + """Create a new workspace. - Asks for name and description if not provided. - Workspace directory uses a slugified version of the name. @@ -458,7 +443,7 @@ def create_workspace( @app.command("list-repos") def list_repos( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -468,8 +453,7 @@ def list_repos( ), ), ): - """ - List repos and branches in the current (or specified) workspace. + """List repos and branches in the current (or specified) workspace. Shows: - repo directory name @@ -506,9 +490,8 @@ def list_repos( # ---------------- add-repo ---------------- -def list_all_repos(repos_dir: Path) -> List[Path]: - """ - List all directories in repos_dir that appear to be git repos. +def list_all_repos(repos_dir: Path) -> list[Path]: + """List all directories in repos_dir that appear to be git repos. """ if not repos_dir.exists(): return [] @@ -519,9 +502,8 @@ def list_all_repos(repos_dir: Path) -> List[Path]: return repos -def pick_repo_with_iterfzf(repos: List[Path]) -> Optional[Path]: - """ - Use iterfzf (Python library) to pick a repo from a list of paths. +def pick_repo_with_iterfzf(repos: list[Path]) -> Path | None: + """Use iterfzf (Python library) to pick a repo from a list of paths. """ if not repos: return None @@ -538,9 +520,8 @@ def pick_repo_with_iterfzf(repos: List[Path]) -> Optional[Path]: return None -def pick_workspace_with_iterfzf(workspaces: List[Path]) -> Optional[Path]: - """ - Use iterfzf (Python library) to pick a workspace from a list of paths. +def pick_workspace_with_iterfzf(workspaces: list[Path]) -> Path | None: + """Use iterfzf (Python library) to pick a workspace from a list of paths. """ names = [w.name for w in workspaces] choice = iterfzf(names, prompt="pick a workspace> ") @@ -558,7 +539,7 @@ def pick_workspace_with_iterfzf(workspaces: List[Path]) -> Optional[Path]: @app.command("add", hidden=True) def add_repo( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -567,7 +548,7 @@ def add_repo( "If omitted, uses the workspace containing the current directory." ), ), - repo_name: Optional[str] = typer.Option( + repo_name: str | None = typer.Option( None, "--repo", "-r", @@ -577,8 +558,7 @@ def add_repo( ), ), ): - """ - Add a repo to a workspace. + """Add a repo to a workspace. - Lists all directories in repos_dir as repos. - Uses iterfzf to pick repo if --repo not given. @@ -602,7 +582,7 @@ def add_repo( console.print(f"[red]No git repos found in {repos_dir}[/red]") raise typer.Exit(1) - repo_path: Optional[Path] = None + repo_path: Path | None = None if repo_name: for r in all_repos: if r.name == repo_name: @@ -665,9 +645,8 @@ def add_repo( # ---------------- rm-workspace ---------------- -def find_repo_for_worktree(worktree_path: Path, repos_dir: Path) -> Optional[Path]: - """ - Try to find the parent repo for a worktree, assuming it lives in repos_dir +def find_repo_for_worktree(worktree_path: Path, repos_dir: Path) -> Path | None: + """Try to find the parent repo for a worktree, assuming it lives in repos_dir with the same directory name as the worktree. """ candidate = repos_dir / worktree_path.name @@ -677,8 +656,7 @@ def find_repo_for_worktree(worktree_path: Path, repos_dir: Path) -> Optional[Pat def has_unpushed_commits(repo_path: Path) -> bool: - """ - Detect if branch has commits not on its upstream. + """Detect if branch has commits not on its upstream. Uses 'git rev-list @{u}..HEAD'; if non-empty, there are unpushed commits. """ # Check if upstream exists @@ -708,8 +686,7 @@ def is_branch_integrated_into_main( branch: str, main_ref: str = "origin/main", ) -> bool: - """ - Heuristic: is `branch`'s content already in `main_ref`? + """Heuristic: is `branch`'s content already in `main_ref`? Returns True if: - branch tip is an ancestor of main_ref (normal merge / FF / rebase-before-merge), OR @@ -761,7 +738,7 @@ def is_branch_integrated_into_main( @app.command("rm") def remove_workspace( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -782,8 +759,7 @@ def remove_workspace( help="Ref to consider as the integration target (default: origin/main).", ), ): - """ - Remove a workspace: + """Remove a workspace: - For each repo worktree in the workspace: * Check for dirty work or unpushed commits. @@ -803,7 +779,7 @@ def remove_workspace( title, _desc = read_workspace_readme(ws_dir) # Collect worktrees (subdirs that look like git repos, excluding readme.md) - worktrees: List[Path] = [] + worktrees: list[Path] = [] for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()): if child.name.lower() == "readme.md": continue @@ -824,7 +800,7 @@ def remove_workspace( raise typer.Exit(0) # Check for dirty / unpushed changes - problems: List[str] = [] + problems: list[str] = [] for wt in worktrees: status = get_git_status(wt) branch = get_current_branch(wt) or "?" @@ -905,9 +881,8 @@ def remove_workspace( # ---------------- wip (stage + commit) ---------------- -def git_status_porcelain(repo_path: Path) -> List[Tuple[str, str]]: - """ - Return a list of (status_code, path) for changes in the repo/worktree. +def git_status_porcelain(repo_path: Path) -> list[tuple[str, str]]: + """Return a list of (status_code, path) for changes in the repo/worktree. Uses 'git status --porcelain'. """ @@ -916,7 +891,7 @@ def git_status_porcelain(repo_path: Path) -> List[Tuple[str, str]]: console.print(f"[red]Failed to get status for {repo_path.name}:[/red]\n{err}") return [] - changes: List[Tuple[str, str]] = [] + changes: list[tuple[str, str]] = [] for line in out.splitlines(): if not line.strip(): continue @@ -926,9 +901,8 @@ def git_status_porcelain(repo_path: Path) -> List[Tuple[str, str]]: return changes -def choose_files_for_wip(repo_path: Path, changes: List[Tuple[str, str]]) -> List[str]: - """ - Interactively choose which files to stage for WIP. +def choose_files_for_wip(repo_path: Path, changes: list[tuple[str, str]]) -> list[str]: + """Interactively choose which files to stage for WIP. - Show list of changed files. - Ask user: @@ -973,7 +947,7 @@ def choose_files_for_wip(repo_path: Path, changes: List[Tuple[str, str]]) -> Lis @app.command("wip") def wip_workspace( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -983,8 +957,7 @@ def wip_workspace( ), ), ): - """ - For each repo in the workspace: + """For each repo in the workspace: - Show list of changed files. - Ask whether to stage all, none, or pick some files. @@ -1006,7 +979,7 @@ def wip_workspace( @app.command("commit") def commit_workspace( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1015,15 +988,14 @@ def commit_workspace( "If omitted, uses the workspace containing the current directory." ), ), - message: Optional[str] = typer.Option( + message: str | None = typer.Option( None, "--message", "-m", help="Commit message to use.", ), ): - """ - For each repo in the workspace: + """For each repo in the workspace: - Show list of changed files. - Ask whether to stage all, none, or pick some files. @@ -1042,7 +1014,7 @@ def commit_workspace( ) raise typer.Exit(1) - worktrees: List[Path] = [] + worktrees: list[Path] = [] for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()): if child.name.lower() == "readme.md": continue @@ -1092,7 +1064,7 @@ def commit_workspace( @app.command("push") def push_workspace( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1108,8 +1080,7 @@ def push_workspace( help="Remote name to push to (default: origin).", ), ): - """ - For each repo in the workspace, run 'git push '. + """For each repo in the workspace, run 'git push '. Skips repos with no current branch or when push fails. """ @@ -1127,7 +1098,7 @@ def push_workspace( f"(dir: {ws_dir.name}) to remote '{remote}'" ) - worktrees: List[Path] = [] + worktrees: list[Path] = [] for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()): if child.name.lower() == "readme.md": continue @@ -1163,7 +1134,7 @@ def push_workspace( @app.command("status") def status_workspace( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1173,8 +1144,7 @@ def status_workspace( ), ), ): - """ - Show detailed status for the current (or specified) workspace. + """Show detailed status for the current (or specified) workspace. For each repo in the workspace: - repo directory name @@ -1208,7 +1178,7 @@ def status_workspace( files_table.add_column("File", justify="left") files_table.add_column("Status", justify="right") - worktrees: List[Path] = [] + worktrees: list[Path] = [] for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()): if child.name.lower() == "readme.md": continue @@ -1245,7 +1215,7 @@ def status_workspace( @app.command("diff") def diff_workspace( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1261,8 +1231,7 @@ def diff_workspace( help="Show only staged changes (git diff --cached).", ), ): - """ - Show git diff for all repos in the workspace. + """Show git diff for all repos in the workspace. Uses rich Syntax highlighter for diffs. """ _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) @@ -1280,7 +1249,7 @@ def diff_workspace( ) # Collect repos - worktrees: List[Path] = [] + worktrees: list[Path] = [] for child in sorted(p for p in ws_dir.iterdir() if p.is_dir()): if child.name.lower() == "readme.md": continue @@ -1357,7 +1326,7 @@ def run_tmux( ) -def get_tmux_sessions() -> List[str]: +def get_tmux_sessions() -> list[str]: """List all tmux sessions.""" result = run_tmux(["list-sessions", "-F", "#{session_name}"]) return result.stdout.decode().splitlines() @@ -1369,7 +1338,7 @@ def session_exists(session_name: str) -> bool: return result.returncode == 0 -def pick_project(base_dir: Path) -> Optional[str]: +def pick_project(base_dir: Path) -> str | None: """Use fzf to pick a subdirectory (project) from base_dir.""" if not base_dir.is_dir(): typer.echo(f"[ta] {base_dir} is not a directory.", err=True) @@ -1405,8 +1374,7 @@ def create_detached_session( path_name: Path, start_mode: bool, ) -> bool: - """ - Create a detached session. + """Create a detached session. - If start_mode: just a single window. - Else: split layout with nvim on top. @@ -1459,8 +1427,7 @@ def create_if_needed_and_attach( path_name: Path, start_mode: bool, ) -> bool: - """ - Mimic the bash logic: + """Mimic the bash logic: - If not in tmux: `tmux new-session -As ... -c path_name` - Else: @@ -1526,7 +1493,7 @@ tmux_app = typer.Typer( @tmux_app.command("attach") def tmux_cli_attach( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1536,8 +1503,7 @@ def tmux_cli_attach( ), ), ): - """ - Attach or create a session for a repo in a workspace. + """Attach or create a session for a repo in a workspace. """ _settings, _repos_dir, workspaces_dir = get_ctx_paths(ctx) ws_dir = find_workspace_dir(workspaces_dir, workspace) @@ -1556,7 +1522,7 @@ def tmux_cli_attach( @tmux_app.command("list-sessions") def tmux_cli_list_sessions( - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1583,7 +1549,7 @@ def tmux_cli_list_sessions( @tmux_app.command("remove") def tmux_cli_remove( - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", @@ -1632,7 +1598,7 @@ app.add_typer(tmux_app) @app.command("attach") def attach( ctx: typer.Context, - workspace: Optional[str] = typer.Option( + workspace: str | None = typer.Option( None, "--workspace", "-w", -- 2.47.3