diff --git a/.claude/commands/plan.md b/.claude/commands/plan.md new file mode 100644 index 0000000..b86ef5a --- /dev/null +++ b/.claude/commands/plan.md @@ -0,0 +1,37 @@ + +Plan how to implement the specified feature. + +This is the second step in the Spec-Driven Development lifecycle. + +Given the implementation details provided as an argument, do this: + +1. Run `scripts/setup-plan.sh --json` from the repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. All future file paths must be absolute. +2. Read and analyze the feature specification to understand: + - The feature requirements and user stories + - Functional and non-functional requirements + - Success criteria and acceptance criteria + - Any technical constraints or dependencies mentioned + +3. Read the constitution at `/memory/constitution.md` to understand constitutional requirements. + +4. Execute the implementation plan template: + - Load `/templates/plan-template.md` (already copied to IMPL_PLAN path) + - Set Input path to FEATURE_SPEC + - Run the Execution Flow (main) function steps 1-10 + - The template is self-contained and executable + - Follow error handling and gate checks as specified + - Let the template guide artifact generation in $SPECS_DIR: + * Phase 0 generates research.md + * Phase 1 generates data-model.md, contracts/, quickstart.md + * Phase 2 generates tasks.md + - Incorporate user-provided details from arguments into Technical Context: $ARGUMENTS + - Update Progress Tracking as you complete each phase + +5. Verify execution completed: + - Check Progress Tracking shows all phases complete + - Ensure all required artifacts were generated + - Confirm no ERROR states in execution + +6. Report results with branch name, file paths, and generated artifacts. + +Use absolute paths with the repository root for all file operations to avoid path issues. diff --git a/.claude/commands/specify.md b/.claude/commands/specify.md new file mode 100644 index 0000000..c46f104 --- /dev/null +++ b/.claude/commands/specify.md @@ -0,0 +1,13 @@ + +Start a new feature by creating a specification and feature branch. + +This is the first step in the Spec-Driven Development lifecycle. + +Given the feature description provided as an argument, do this: + +1. Run the script `scripts/create-new-feature.sh --json "$ARGUMENTS"` from repo root and parse its JSON output for BRANCH_NAME and SPEC_FILE. All file paths must be absolute. +2. Load `templates/spec-template.md` to understand required sections. +3. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings. +4. Report completion with branch name, spec file path, and readiness for the next phase. + +Note: The script creates and checks out the new branch and initializes the spec file before writing. diff --git a/.claude/commands/tasks.md b/.claude/commands/tasks.md new file mode 100644 index 0000000..1fd64be --- /dev/null +++ b/.claude/commands/tasks.md @@ -0,0 +1,59 @@ + +Break down the plan into executable tasks. + +This is the third step in the Spec-Driven Development lifecycle. + +Given the context provided as an argument, do this: + +1. Run `scripts/check-task-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. +2. Load and analyze available design documents: + - Always read plan.md for tech stack and libraries + - IF EXISTS: Read data-model.md for entities + - IF EXISTS: Read contracts/ for API endpoints + - IF EXISTS: Read research.md for technical decisions + - IF EXISTS: Read quickstart.md for test scenarios + + Note: Not all projects have all documents. For example: + - CLI tools might not have contracts/ + - Simple libraries might not need data-model.md + - Generate tasks based on what's available + +3. Generate tasks following the template: + - Use `/templates/tasks-template.md` as the base + - Replace example tasks with actual tasks based on: + * **Setup tasks**: Project init, dependencies, linting + * **Test tasks [P]**: One per contract, one per integration scenario + * **Core tasks**: One per entity, service, CLI command, endpoint + * **Integration tasks**: DB connections, middleware, logging + * **Polish tasks [P]**: Unit tests, performance, docs + +4. Task generation rules: + - Each contract file → contract test task marked [P] + - Each entity in data-model → model creation task marked [P] + - Each endpoint → implementation task (not parallel if shared files) + - Each user story → integration test marked [P] + - Different files = can be parallel [P] + - Same file = sequential (no [P]) + +5. Order tasks by dependencies: + - Setup before everything + - Tests before implementation (TDD) + - Models before services + - Services before endpoints + - Core before integration + - Everything before polish + +6. Include parallel execution examples: + - Group [P] tasks that can run together + - Show actual Task agent commands + +7. Create FEATURE_DIR/tasks.md with: + - Correct feature name from implementation plan + - Numbered tasks (T001, T002, etc.) + - Clear file paths for each task + - Dependency notes + - Parallel execution guidance + +Context for task generation: $ARGUMENTS + +The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context. diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fb120b8..9a517b5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -14,7 +14,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 @@ -24,23 +24,27 @@ jobs: with: python-version: ${{ matrix.python-version }} - - name: Install dependencies + - name: Install uv + uses: astral-sh/setup-uv@v3 + with: + version: "latest" + + - name: Install dependencies using uv run: | - python -m pip install --upgrade pip - pip install -e .[dev] + uv sync --extra dev - name: Lint with ruff run: | - ruff check . - ruff format --check . + uv run ruff check . + uv run ruff format --check . - name: Type check with pyrefly run: | - pyrefly check + uv run pyrefly check - name: Test with pytest run: | - pytest --cov=kicad_lib_manager --cov-report=xml + uv run pytest --cov=kicad_lib_manager --cov-report=xml - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' diff --git a/.gitignore b/.gitignore index 41c9af8..27c5b88 100644 --- a/.gitignore +++ b/.gitignore @@ -45,5 +45,7 @@ test_lib/ test_template/ TODO.md -CLAUDE.md -.claude/ \ No newline at end of file +.claude/settings.local.json +.claude/doc/ +temp/ +templates/ \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a218089 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,270 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Core Principles: Simplicity & Type Safety + +KiLM (KiCad Library Manager) prioritizes **professional code standards** and **complete type safety**: + +- **Type-first development**: Everything uses proper type hints - no `Any`, no dynamic typing +- **Professional standards**: No emojis in code, no hardcoded values, proper constants +- **CLI-focused**: Simple, reliable command-line interface for KiCad library management +- **Cross-platform**: Support for Windows, macOS, Linux KiCad installations + +## Architecture Overview + +KiLM is a **command-line tool for managing KiCad libraries** across projects and workstations: + +**Core Functionality**: +- KiCad configuration detection across platforms +- Library management (symbol and footprint libraries) +- Environment variable configuration +- Project template management +- Backup and restore of KiCad configurations + +## Development Commands + +### Python CLI Development +```bash +# Install Python package in development mode +pip install -e ".[dev]" + +# Install pre-commit hooks (once implemented) +pre-commit install + +# Test basic functionality +kilm status +kilm --help +``` + +### Testing & Quality +```bash +# Run tests with coverage +pytest --cov=kicad_lib_manager --cov-report=html + +# Type checking, formatting, and linting +pyrefly # Type check (required - no "any" types allowed) +ruff format . +ruff check . + +# All quality checks +pre-commit run --all-files +``` + +### CI/CD and Releases +```bash +# Create a new release (automated via GitHub Actions) +# 1. Update version in kicad_lib_manager/__init__.py +# 2. Push version tag: +git tag v0.3.1 +git push origin v0.3.1 + +# This triggers: +# - Automated testing and quality checks +# - PyPI publishing with trusted publishing +# - Draft GitHub release with auto-generated notes +# - Multi-platform compatibility verification +``` + +**Release Process:** +1. **Tag-based releases**: Push version tags to trigger automated releases +2. **Draft releases**: GitHub releases created as drafts for manual review +3. **Automatic PyPI**: Publishes to PyPI immediately via trusted publishing +4. **Auto-generated notes**: Release notes include commits, PRs, and install instructions + +## Code Quality Standards + +### Type Safety Requirements +- **No Any types**: All functions must have proper type hints +- **Pydantic models**: Use Pydantic for configuration validation where applicable +- **Type checking**: Must pass pyrefly type checking without errors + +### Professional Code Standards +- **No emojis**: Keep code and output professional - avoid emojis in code, comments, or CLI output +- **No hardcoding**: Use constants, configuration files, or environment variables +- **Proper error handling**: Consistent error patterns with informative messages +- **Cross-platform paths**: Use pathlib.Path for all file operations +- **Context7**: Often use context 7 MCP when dealing with new code and packages + +## CLI Architecture + +### Command Structure +``` +kilm # Main CLI entry point +├── init # Initialize library +├── setup # Configure KiCad to use libraries +├── status # Show current configuration +├── list # List available libraries +├── pin/unpin # Pin/unpin favorite libraries +├── add-3d # Add 3D model libraries +├── config # Configuration management +├── sync # Update/sync library content (was 'update') +├── update # Update KiLM itself (breaking change in 0.4.0) +├── add-hook # Add project hooks +└── template # Project template management +``` + +## BREAKING CHANGES in v0.4.0 + +### Command Restructuring +- **`kilm update`** now updates KiLM itself (self-update functionality) +- **`kilm sync`** updates library content (was `kilm update`) +- Added deprecation banner for transition period +- Full auto-update functionality with installation method detection + +### New Features +- **Self-Update System**: Detects installation method (pip, pipx, conda, uv, homebrew) +- **PyPI Integration**: Checks for latest versions with proper caching +- **Update Preferences**: Configurable update checking and frequency +- **Professional UX**: Non-intrusive notifications with method-specific guidance + +### Core Modules +- **CLI Layer** (`main.py`): Typer-based command interface with Rich output +- **Commands** (`commands/`): Individual command implementations +- **Library Manager** (`library_manager.py`): Core library management logic +- **Configuration** (`config.py`): KiCad configuration handling with update preferences +- **Auto-Update** (`auto_update.py`): Self-update functionality with installation detection +- **Utilities** (`utils/`): File operations, backups, metadata, templates + +## Development Workflow - MANDATORY + +### Task Documentation +- **ALWAYS create task file**: For ANY work request, immediately create `.claude/doc/tasks/[date]-[seq]-[task-name].md` +- **Update throughout**: Document progress, decisions, blockers in real-time +- **Include context**: Always pass current task file path to agents for context sharing + +### Code Quality Workflow +- **After completing ANY code changes**: Prompt user "Should I run the code-reviewer to check for quality issues?" +- **Never assume**: Don't run code-reviewer automatically without asking +- **Update task file**: Document review results and any issues found + +### Agent Management +- **Use agents for all complex tasks**: Code reviews, documentation, analysis, implementation planning +- **Always provide task context**: Give agents the current task file path when applicable +- **Delegate, don't duplicate**: Use specialized agents instead of handling complex tasks directly +- **Give quality context to agents**: Point them to current task, tell them to use linters and type checkers + +### Agent Context Management (Required) + +- **Persist context in repo (important)**: Agents MUST save context under `.claude/doc/` to ensure continuity across runs. + - Tasks: `.claude/doc/tasks/YYYY-MM-DD-SEQ-slug.md` (SEQ is a 3-digit daily sequence starting at `001`. Agents only have the date—remember to increment SEQ if multiple tasks are created on the same day.) + - Decisions/ADR: `.claude/doc/adr/ADR-####-short-title.md` + - General agent reports: `.claude/doc/agent-reports/YYYY-MM-DD-SEQ-agent-report.md` +- **Pass context to agents (including general agents)**: When invoking any agent, ALWAYS include the path to the current task file. If the agent has no repo-specific instructions, explicitly tell it to save an `agent-report` (and append a short update to the task) using the same naming scheme. +- **Round-trip updates**: After each agent step, append a concise update (what changed, why, next) to the active task file. +- **Minimal duplication**: Reference prior notes; keep canonical decisions in ADRs and link from task files. +- **Example context**: + - Current task: `.claude/doc/tasks/YYYY-MM-DD-SEQ-task-name.md` + - Agent should also write: `.claude/doc/agent-reports/YYYY-MM-DD-SEQ-agent-report.md` + - Instruction: "Always provide current task path and increment SEQ for same-day tasks." + +## Key Implementation Patterns + +### KiCad Configuration Management +```python +# Professional, type-safe configuration handling +from pathlib import Path +from typing import Dict, List, Optional +from pydantic import BaseModel + +class KiCadConfig(BaseModel): + """Type-safe KiCad configuration model.""" + libraries: List[str] + environment_vars: Dict[str, str] + backup_enabled: bool = True + +def detect_kicad_config_path() -> Optional[Path]: + """Detect KiCad configuration path across platforms.""" + # Cross-platform detection logic + pass +``` + +### Library Management +```python +# Type-safe library operations +from typing import Protocol + +class LibraryManager(Protocol): + """Protocol for library management operations.""" + + def add_library(self, library_path: Path, library_type: str) -> bool: + """Add library to KiCad configuration.""" + ... + + def remove_library(self, library_name: str) -> bool: + """Remove library from KiCad configuration.""" + ... +``` + +## SOLID Principles for CLI Tools + +- **S**ingle Responsibility: Each command does one thing well +- **O**pen/Closed: Extensible for new KiCad features without modifying core +- **L**iskov Substitution: All library types implement consistent interfaces +- **I**nterface Segregation: Small, focused command interfaces +- **D**ependency Inversion: Depend on abstractions, not concrete implementations + +## Integration with KiCad + +### KiCad Version Support +- **KiCad 9.x**: Primary support target +- **KiCad 8.x**: Full compatibility + +### Configuration Files +- **Symbol libraries**: `.kicad_sym` files in symbol table +- **Footprint libraries**: `.pretty` directories in footprint table +- **Environment variables**: `kicad_common.json` configuration +- **Project templates**: Template directory management + +## Adding New Commands + +**Step 1**: Create command module +```python +# commands/new_command/command.py +from typing import Annotated, Optional +import typer +from rich.console import Console + +console = Console() + +def new_command( + option: Annotated[ + Optional[str], + typer.Option("--option", help="Command option") + ] = None, +) -> None: + """New command description.""" + # Implementation with proper type hints and Rich output + console.print("Command executed successfully!") +``` + +**Step 2**: Register in CLI +```python +# main.py - Add to the CLI app +from .commands.new_command.command import new_command + +app.command("new-command")(new_command) +``` + +**Step 3**: Add tests +```python +# tests/test_new_command.py +def test_new_command(): + """Test new command functionality.""" + # Comprehensive test coverage + pass +``` + +## Modernization Roadmap + +### Phase 1: Infrastructure +- Modern build system (pyproject.toml with hatchling) +- Complete type safety (zero Any types, pyrefly validation) +- Professional code standards (no emojis, constants extracted) +- Comprehensive documentation structure + +### Phase 2: Enhancement +- Modern CLI framework (Typer + Rich for better UX) +- Development tooling (pre-commit hooks, quality pipeline) +- Enhanced error handling and user experience +- Cross-platform support improvements \ No newline at end of file diff --git a/PLAN.md b/PLAN.md index 48c970f..dd5d766 100644 --- a/PLAN.md +++ b/PLAN.md @@ -65,7 +65,7 @@ KiLM follows a **simple, focused command structure**: - **`kilm pin `** - Pin favorite libraries for quick access - **`kilm unpin `** - Unpin libraries - **`kilm add-3d `** - Add 3D model libraries -- **`kilm update`** - Update library definitions +- **`kilm sync`** - Update library definitions ### Configuration & Templates - **`kilm config`** - Manage configuration settings diff --git a/docs/src/assets/cli-config-list-verbose.png b/docs/src/assets/cli-config-list-verbose.png new file mode 100644 index 0000000..ced4228 Binary files /dev/null and b/docs/src/assets/cli-config-list-verbose.png differ diff --git a/docs/src/assets/cli-help.png b/docs/src/assets/cli-help.png new file mode 100644 index 0000000..84c9ca7 Binary files /dev/null and b/docs/src/assets/cli-help.png differ diff --git a/docs/src/assets/cli-sync.png b/docs/src/assets/cli-sync.png new file mode 100644 index 0000000..aaea670 Binary files /dev/null and b/docs/src/assets/cli-sync.png differ diff --git a/docs/src/assets/cli-welcome.png b/docs/src/assets/cli-welcome.png new file mode 100644 index 0000000..e0d2545 Binary files /dev/null and b/docs/src/assets/cli-welcome.png differ diff --git a/docs/src/content/docs/community/development.mdx b/docs/src/content/docs/community/development.mdx index 0dc7881..c9286b4 100644 --- a/docs/src/content/docs/community/development.mdx +++ b/docs/src/content/docs/community/development.mdx @@ -9,7 +9,7 @@ This guide covers setting up a professional development environment for KiLM wit ## Prerequisites -- **Python 3.8+**: Required for KiLM development +- **Python 3.9+**: Required for KiLM development - **Git**: For version control and contribution workflow - **KiCad 6.x+**: For testing KiLM integration (run at least once) @@ -136,7 +136,7 @@ KiLM follows a **type-first development** approach. All code must have proper ty ## Project Architecture ### Core Modules -- **`cli.py`**: Click-based command interface +- **`cli.py`**: Typer-based command interface (modern CLI with better help/UX) - **`commands/`**: Individual command implementations - **`library_manager.py`**: Core library management logic - **`config.py`**: KiCad configuration handling diff --git a/docs/src/content/docs/guides/getting-started.mdx b/docs/src/content/docs/guides/getting-started.mdx index aeb0525..249ef59 100644 --- a/docs/src/content/docs/guides/getting-started.mdx +++ b/docs/src/content/docs/guides/getting-started.mdx @@ -10,10 +10,30 @@ This guide covers the essential workflows for setting up and using KiLM (KiCad L ## Prerequisites - **KiCad 6.x or newer:** Must be run at least once to generate configuration files -- **Python 3.8 or newer:** Required for KiLM installation and execution +- **Python 3.9 or newer:** Required for KiLM installation and execution - **Git:** Optional, but recommended for library version control and collaboration - **KiLM:** [Install KiLM](/guides/installation/) using pip, pipx, or uv +## CLI showcase + +A quick visual tour of the CLI experience: + +### 1) `kilm` + +![KiLM welcome output](../../../assets/cli-welcome.png) + +### 2) `kilm --help` + +![KiLM global help](../../../assets/cli-help.png) + +### 3) `kilm config list -v` + +![Config listing with verbose details](../../../assets/cli-config-list-verbose.png) + +### 4) `kilm sync` + +![Syncing libraries](../../../assets/cli-sync.png) + ## Understanding User Roles (Creator vs. Consumer) KiLM is designed for teams or individuals managing KiCad libraries. We can think of two main roles: diff --git a/docs/src/content/docs/guides/installation.mdx b/docs/src/content/docs/guides/installation.mdx index dcb028b..8001fa3 100644 --- a/docs/src/content/docs/guides/installation.mdx +++ b/docs/src/content/docs/guides/installation.mdx @@ -5,11 +5,11 @@ description: How to install KiLM on your system. import { Aside } from "@astrojs/starlight/components"; -KiLM (KiCad Library Manager) is a Python CLI tool that requires Python 3.8+ and KiCad 6.x+ to be installed and run at least once. +KiLM (KiCad Library Manager) is a Python CLI tool that requires Python 3.9+ and KiCad 6.x+ to be installed and run at least once. ## System Requirements -- **Python 3.8 or newer** +- **Python 3.9 or newer** - **KiCad 6.x or newer** (must be launched at least once to create configuration directories) - **Operating System:** Windows, macOS, or Linux diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 9e34065..b73a38a 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -35,7 +35,7 @@ features: details: Share libraries via Git with consistent metadata and environment setup. - icon: tabler:terminal-2 title: Modern CLI Design - details: Intuitive commands with comprehensive help, dry-run modes, and clear feedback. + details: Intuitive Typer-powered CLI with improved UI/UX, comprehensive help, dry-run modes, and clear feedback. --- import { Card, CardGrid } from "@astrojs/starlight/components"; diff --git a/kicad_lib_manager/__main__.py b/kicad_lib_manager/__main__.py index add54db..840bbfa 100644 --- a/kicad_lib_manager/__main__.py +++ b/kicad_lib_manager/__main__.py @@ -2,7 +2,7 @@ Entry point for running the package as a module """ -from .cli import main +from .main import app if __name__ == "__main__": - main() + app() diff --git a/kicad_lib_manager/cli.py b/kicad_lib_manager/cli.py deleted file mode 100644 index 90d95d7..0000000 --- a/kicad_lib_manager/cli.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env python3 -""" -Command-line interface for KiCad Library Manager -""" - -import importlib.metadata - -import click - -from .commands.add_3d import add_3d -from .commands.add_hook import add_hook -from .commands.config import config -from .commands.init import init -from .commands.list_libraries import list_cmd -from .commands.pin import pin -from .commands.setup import setup -from .commands.status import status -from .commands.sync import sync -from .commands.template import template -from .commands.unpin import unpin -from .commands.update import update - - -@click.group() -@click.version_option(version=importlib.metadata.version("kilm")) -def main(): - """KiCad Library Manager - Manage KiCad libraries - - This tool helps configure and manage KiCad libraries across your projects. - """ - pass - - -# Register commands -main.add_command(setup) -main.add_command(list_cmd, name="list") -main.add_command(status) -main.add_command(pin) -main.add_command(unpin) -main.add_command(init) -main.add_command(add_3d) -main.add_command(config) -main.add_command(sync) -main.add_command(update) -main.add_command(add_hook) -main.add_command(template) - - -if __name__ == "__main__": - main() diff --git a/kicad_lib_manager/commands/add_3d/__init__.py b/kicad_lib_manager/commands/add_3d/__init__.py index 8676021..d60f0dd 100644 --- a/kicad_lib_manager/commands/add_3d/__init__.py +++ b/kicad_lib_manager/commands/add_3d/__init__.py @@ -1,3 +1,13 @@ +import typer + from .command import add_3d -__all__ = ["add_3d"] +add_3d_app = typer.Typer( + name="add-3d", + help="Add cloud-based 3D model libraries to KiCad configuration", + rich_markup_mode="rich", + callback=add_3d, + invoke_without_command=True, +) + +__all__ = ["add_3d", "add_3d_app"] diff --git a/kicad_lib_manager/commands/add_3d/command.py b/kicad_lib_manager/commands/add_3d/command.py index 84f2f76..2f35c30 100644 --- a/kicad_lib_manager/commands/add_3d/command.py +++ b/kicad_lib_manager/commands/add_3d/command.py @@ -1,13 +1,14 @@ """ -Add cloud-based 3D models directory command for KiCad Library Manager. +Add cloud-based 3D models directory command for KiCad Library Manager (Typer version). """ -import sys from pathlib import Path +from typing import Annotated, Optional -import click +import typer +from rich.console import Console -from ...config import Config +from ...services.config_service import Config from ...utils.metadata import ( CLOUD_METADATA_FILE, generate_env_var_name, @@ -16,43 +17,46 @@ write_cloud_metadata, ) - -@click.command() -@click.option( - "--name", - help="Name for this 3D models collection (automatic if not provided)", - default=None, -) -@click.option( - "--directory", - help="Directory containing 3D models (default: current directory)", - type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), -) -@click.option( - "--description", - help="Description for this 3D models collection", - default=None, -) -@click.option( - "--env-var", - help="Custom environment variable name for this 3D model library", - default=None, -) -@click.option( - "--force", - is_flag=True, - default=False, - help="Overwrite existing metadata file if present", - show_default=True, -) -@click.option( - "--no-env-var", - is_flag=True, - default=False, - help="Don't assign an environment variable to this library", - show_default=True, -) -def add_3d(name, directory, description, env_var, force, no_env_var): +console = Console() + + +def add_3d( + name: Annotated[ + Optional[str], + typer.Option( + help="Name for this 3D models collection (automatic if not provided)" + ), + ] = None, + directory: Annotated[ + Optional[Path], + typer.Option( + help="Directory containing 3D models (default: current directory)", + exists=True, + file_okay=False, + dir_okay=True, + resolve_path=True, + ), + ] = None, + description: Annotated[ + Optional[str], typer.Option(help="Description for this 3D models collection") + ] = None, + env_var: Annotated[ + Optional[str], + typer.Option( + "--env-var", + help="Custom environment variable name for this 3D model library", + ), + ] = None, + force: Annotated[ + bool, typer.Option(help="Overwrite existing metadata file if present") + ] = False, + no_env_var: Annotated[ + bool, + typer.Option( + "--no-env-var", help="Don't assign an environment variable to this library" + ), + ] = False, +) -> None: """Add a cloud-based 3D models directory to the configuration. This command registers a directory containing 3D models that are typically @@ -68,41 +72,51 @@ def add_3d(name, directory, description, env_var, force, no_env_var): If no directory is specified, the current directory will be used. """ # Use current directory if not specified - directory = Path.cwd().resolve() if not directory else Path(directory).resolve() + directory = Path.cwd().resolve() if directory is None else directory.resolve() - click.echo(f"Adding cloud-based 3D models directory: {directory}") + console.print( + f"[bold cyan]Adding cloud-based 3D models directory:[/bold cyan] {directory}" + ) # Check for existing metadata metadata = read_cloud_metadata(directory) if metadata and not force: - click.echo(f"Found existing metadata file ({CLOUD_METADATA_FILE}).") + console.print( + f"[green]Found existing metadata file[/green] ({CLOUD_METADATA_FILE})" + ) library_name = metadata.get("name") library_description = metadata.get("description") library_env_var = metadata.get("env_var") - click.echo(f"Using existing name: {library_name}") + console.print(f"Using existing name: [blue]{library_name}[/blue]") # Show environment variable if present if library_env_var and not no_env_var: - click.echo(f"Using existing environment variable: {library_env_var}") + console.print( + f"Using existing environment variable: [yellow]{library_env_var}[/yellow]" + ) # Override with command line parameters if provided if name: library_name = name - click.echo(f"Overriding with provided name: {library_name}") + console.print(f"Overriding with provided name: [blue]{library_name}[/blue]") if description: library_description = description - click.echo(f"Overriding with provided description: {library_description}") + console.print( + f"Overriding with provided description: [blue]{library_description}[/blue]" + ) if env_var: library_env_var = env_var - click.echo( - f"Overriding with provided environment variable: {library_env_var}" + console.print( + f"Overriding with provided environment variable: [yellow]{library_env_var}[/yellow]" ) elif no_env_var: library_env_var = None - click.echo("Disabling environment variable as requested") + console.print( + "[yellow]Disabling environment variable as requested[/yellow]" + ) # Update metadata if command line parameters were provided if name or description or env_var or no_env_var: @@ -114,13 +128,17 @@ def add_3d(name, directory, description, env_var, force, no_env_var): metadata["env_var"] = None metadata["updated_with"] = "kilm" write_cloud_metadata(directory, metadata) - click.echo("Updated metadata file with new information.") + console.print("[green]Updated metadata file with new information[/green]") else: # Create a new metadata file if metadata and force: - click.echo(f"Overwriting existing metadata file ({CLOUD_METADATA_FILE}).") + console.print( + f"[yellow]Overwriting existing metadata file[/yellow] ({CLOUD_METADATA_FILE})" + ) else: - click.echo(f"Creating new metadata file ({CLOUD_METADATA_FILE}).") + console.print( + f"[green]Creating new metadata file[/green] ({CLOUD_METADATA_FILE})" + ) # Generate metadata metadata = get_default_cloud_metadata(directory) @@ -142,7 +160,7 @@ def add_3d(name, directory, description, env_var, force, no_env_var): # Write metadata file write_cloud_metadata(directory, metadata) - click.echo("Metadata file created.") + console.print("[green]Metadata file created[/green]") library_name = metadata["name"] library_env_var = metadata.get("env_var") @@ -158,10 +176,12 @@ def add_3d(name, directory, description, env_var, force, no_env_var): break if not found_models: - click.echo("Warning: No 3D model files found in this directory.") - if not click.confirm("Continue anyway?", default=True): - click.echo("Operation cancelled.") - sys.exit(0) + console.print( + "[yellow]Warning: No 3D model files found in this directory[/yellow]" + ) + if not typer.confirm("Continue anyway?", default=True): + console.print("[red]Operation cancelled[/red]") + raise typer.Exit(0) # Update metadata with actual model count model_count = 0 @@ -177,24 +197,37 @@ def add_3d(name, directory, description, env_var, force, no_env_var): # Add as a cloud-based 3D model library if library_name is None: library_name = metadata.get("name", directory.name) - config.add_library(library_name, str(directory), "cloud") - click.echo(f"3D models directory '{library_name}' added successfully!") - click.echo(f"Path: {directory}") + # Ensure library_name is a string + final_library_name = ( + str(library_name) if library_name is not None else directory.name + ) + config.add_library(final_library_name, str(directory), "cloud") + + console.print( + f"[bold green]3D models directory '{final_library_name}' added successfully![/bold green]" + ) + console.print(f"[blue]Path:[/blue] {directory}") if model_count > 0: - click.echo(f"Found {model_count} 3D model files.") + console.print(f"[green]Found {model_count} 3D model files[/green]") if library_env_var: - click.echo(f"Assigned environment variable: {library_env_var}") - click.echo("\nYou can use this directory with:") - click.echo(f" kilm setup --3d-lib-dirs '{library_name}'") - click.echo(" # or by setting the environment variable") - click.echo(f" export {library_env_var}='{directory}'") + console.print( + f"[yellow]Assigned environment variable:[/yellow] {library_env_var}" + ) + console.print("\n[bold]You can use this directory with:[/bold]") + console.print( + f" [cyan]kilm setup --threed-lib-dirs '{library_name}'[/cyan]" + ) + console.print(" [dim]# or by setting the environment variable[/dim]") + console.print(f" [cyan]export {library_env_var}='{directory}'[/cyan]") # Show current cloud libraries libraries = config.get_libraries("cloud") if len(libraries) > 1: - click.echo("\nAll registered cloud-based 3D model directories:") + console.print( + "\n[bold]All registered cloud-based 3D model directories:[/bold]" + ) for lib in libraries: lib_name = lib.get("name", "unnamed") lib_path = lib.get("path", "unknown") @@ -209,9 +242,11 @@ def add_3d(name, directory, description, env_var, force, no_env_var): pass if lib_env_var: - click.echo(f" - {lib_name}: {lib_path} (ENV: {lib_env_var})") + console.print( + f" - [cyan]{lib_name}[/cyan]: {lib_path} [yellow](ENV: {lib_env_var})[/yellow]" + ) else: - click.echo(f" - {lib_name}: {lib_path}") + console.print(f" - [cyan]{lib_name}[/cyan]: {lib_path}") except Exception as e: - click.echo(f"Error adding 3D models directory: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error adding 3D models directory: {e}[/red]") + raise typer.Exit(1) from e diff --git a/kicad_lib_manager/commands/add_hook/__init__.py b/kicad_lib_manager/commands/add_hook/__init__.py index e802f9f..d7eeab3 100644 --- a/kicad_lib_manager/commands/add_hook/__init__.py +++ b/kicad_lib_manager/commands/add_hook/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import add_hook -__all__ = ["add_hook"] +# Create Typer app and add the add_hook command as callback +add_hook_app = typer.Typer( + name="add-hook", + help="Add a Git post-merge hook to automatically sync KiCad libraries", + rich_markup_mode="rich", + callback=add_hook, + invoke_without_command=True, +) + +__all__ = ["add_hook", "add_hook_app"] diff --git a/kicad_lib_manager/commands/add_hook/command.py b/kicad_lib_manager/commands/add_hook/command.py index d7194ef..6a001c8 100644 --- a/kicad_lib_manager/commands/add_hook/command.py +++ b/kicad_lib_manager/commands/add_hook/command.py @@ -4,8 +4,10 @@ """ from pathlib import Path +from typing import Annotated, Optional -import click +import typer +from rich.console import Console from ...utils.git_utils import ( backup_existing_hook, @@ -14,22 +16,20 @@ merge_hook_content, ) +console = Console() -@click.command() -@click.option( - "--directory", - help="Target git repository directory (defaults to current directory)", - default=None, - type=click.Path(exists=True, file_okay=False, dir_okay=True, resolve_path=True), -) -@click.option( - "--force", - is_flag=True, - default=False, - help="Overwrite existing hook if present", - show_default=True, -) -def add_hook(directory, force): + +def add_hook( + directory: Annotated[ + Optional[Path], + typer.Option( + help="Target git repository directory (defaults to current directory)" + ), + ] = None, + force: Annotated[ + bool, typer.Option(help="Overwrite existing hook if present") + ] = False, +) -> None: """Add a Git post-merge hook to automatically sync KiCad libraries. This command adds a Git post-merge hook to the specified repository @@ -39,78 +39,100 @@ def add_hook(directory, force): This ensures your KiCad libraries are always up-to-date after pulling changes from remote repositories. """ - # Determine target directory - target_dir = Path(directory) if directory else Path.cwd() - - click.echo(f"Adding Git hook to repository: {target_dir}") - try: - # Get the active hooks directory (handles custom paths, worktrees, etc.) - hooks_dir = get_git_hooks_directory(target_dir) - click.echo(f"Using hooks directory: {hooks_dir}") + # Determine target directory + target_dir = directory if directory else Path.cwd() - except RuntimeError as e: - raise click.ClickException(f"Error: {e}") from e + console.print(f"[cyan]Adding Git hook to repository: {target_dir}[/cyan]") - # Check if post-merge hook already exists - post_merge_hook = hooks_dir / "post-merge" - - if post_merge_hook.exists(): - if not force: - click.echo(f"Post-merge hook already exists at {post_merge_hook}") - if not click.confirm("Overwrite existing hook?", default=False): - click.echo("Hook installation cancelled.") - return + try: + # Get the active hooks directory (handles custom paths, worktrees, etc.) + hooks_dir = get_git_hooks_directory(target_dir) + console.print(f"[blue]Using hooks directory: {hooks_dir}[/blue]") - # Create backup of existing hook - backup_path = backup_existing_hook(post_merge_hook) - click.echo(f"Created backup of existing hook: {backup_path}") + except RuntimeError as e: + console.print(f"[red]Error: {e}[/red]") + raise typer.Exit(1) from e - # Read existing content for potential merging - try: - existing_content = post_merge_hook.read_text(encoding="utf-8") + # Check if post-merge hook already exists + post_merge_hook = hooks_dir / "post-merge" - if force: - # Force overwrite - don't merge, just replace - click.echo("Force overwrite requested, replacing existing hook...") - new_content = create_kilm_hook_content() - else: - # Merge with existing content to preserve user logic - click.echo("Merging KiLM content with existing hook...") - new_content = merge_hook_content( - existing_content, create_kilm_hook_content() + if post_merge_hook.exists(): + if not force: + console.print( + f"[yellow]Post-merge hook already exists at {post_merge_hook}[/yellow]" ) + overwrite = typer.confirm("Overwrite existing hook?", default=False) + if not overwrite: + console.print("[yellow]Hook installation cancelled.[/yellow]") + return + + # Create backup of existing hook + backup_path = backup_existing_hook(post_merge_hook) + console.print( + f"[green]Created backup of existing hook: {backup_path}[/green]" + ) - except (OSError, UnicodeDecodeError): - click.echo("Warning: Could not read existing hook content, overwriting...") + # Read existing content for potential merging + try: + existing_content = post_merge_hook.read_text(encoding="utf-8") + + if force: + # Force overwrite - don't merge, just replace + console.print( + "[yellow]Force overwrite requested, replacing existing hook...[/yellow]" + ) + new_content = create_kilm_hook_content() + else: + # Merge with existing content to preserve user logic + console.print( + "[cyan]Merging KiLM content with existing hook...[/cyan]" + ) + new_content = merge_hook_content( + existing_content, create_kilm_hook_content() + ) + + except (OSError, UnicodeDecodeError): + console.print( + "[yellow]Warning: Could not read existing hook content, overwriting...[/yellow]" + ) + new_content = create_kilm_hook_content() + else: + # No existing hook, create new one new_content = create_kilm_hook_content() - else: - # No existing hook, create new one - new_content = create_kilm_hook_content() - try: - # Write the hook content - with post_merge_hook.open("w") as f: - f.write(new_content) + try: + # Write the hook content + with post_merge_hook.open("w") as f: + f.write(new_content) - # Make the hook executable - post_merge_hook.chmod(0o755) + # Make the hook executable + post_merge_hook.chmod(0o755) - click.echo(f"Successfully installed post-merge hook at {post_merge_hook}") - click.echo( - "The hook will run 'kilm sync' after every 'git pull' or 'git merge' operation." - ) + console.print( + f"[green]Successfully installed post-merge hook at {post_merge_hook}[/green]" + ) + console.print( + "[blue]The hook will run 'kilm sync' after every 'git pull' or 'git merge' operation.[/blue]" + ) + + if post_merge_hook.exists() and "KiLM-managed section" in new_content: + console.print( + "\n[cyan]Note: The hook contains clear markers for KiLM-managed sections,[/cyan]" + ) + console.print("[cyan]making future updates safe and idempotent.[/cyan]") - if post_merge_hook.exists() and "KiLM-managed section" in new_content: - click.echo( - "\nNote: The hook contains clear markers for KiLM-managed sections," + console.print( + "\n[blue]Note: You may need to modify the hook script if you want to customize[/blue]" + ) + console.print( + "[blue]the update behavior or automatically set up libraries.[/blue]" ) - click.echo("making future updates safe and idempotent.") - click.echo( - "\nNote: You may need to modify the hook script if you want to customize" - ) - click.echo("the update behavior or automatically set up libraries.") + except Exception as e: + console.print(f"[red]Error creating hook: {str(e)}[/red]") + raise typer.Exit(1) from e except Exception as e: - raise click.ClickException(f"Error creating hook: {str(e)}") from e + console.print(f"[red]Error: {e}[/red]") + raise typer.Exit(1) from e diff --git a/kicad_lib_manager/commands/config/__init__.py b/kicad_lib_manager/commands/config/__init__.py index 6a0af2a..e5b7a23 100644 --- a/kicad_lib_manager/commands/config/__init__.py +++ b/kicad_lib_manager/commands/config/__init__.py @@ -1,3 +1,20 @@ -from .command import config +import typer -__all__ = ["config"] +from .command import list_config, remove, set_default + +# Create Typer app with subcommands +config_app = typer.Typer( + name="config", + help="Manage KiCad Library Manager configuration", + rich_markup_mode="rich", + no_args_is_help=True, +) + +# Add subcommands +config_app.command("list", help="List all configured libraries in kilm")(list_config) +config_app.command("set-default", help="Set a library as the default for operations")( + set_default +) +config_app.command("remove", help="Remove a library from the configuration")(remove) + +__all__ = ["config_app", "list_config", "set_default", "remove"] diff --git a/kicad_lib_manager/commands/config/command.py b/kicad_lib_manager/commands/config/command.py index 1b59c38..e10191b 100644 --- a/kicad_lib_manager/commands/config/command.py +++ b/kicad_lib_manager/commands/config/command.py @@ -5,10 +5,14 @@ import sys from pathlib import Path +from typing import Annotated, Optional -import click +import typer +from rich.console import Console +from rich.panel import Panel +from rich.table import Table -from ...config import Config +from ...services.config_service import Config from ...utils.metadata import ( CLOUD_METADATA_FILE, GITHUB_METADATA_FILE, @@ -16,33 +20,22 @@ read_github_metadata, ) - -@click.group() -def config(): - """Manage KiCad Library Manager configuration. - - This command group allows you to list, set defaults, and remove - configuration entries for KiCad Library Manager. - """ - pass - - -@config.command() -@click.option( - "--type", - "library_type", - type=click.Choice(["github", "cloud", "all"]), - default="all", - help="Type of libraries to list (github=symbols/footprints, cloud=3D models)", - show_default=True, -) -@click.option( - "--verbose", - "-v", - is_flag=True, - help="Show more information about libraries", -) -def list(library_type, verbose): +console = Console() + + +def list_config( + library_type: Annotated[ + str, + typer.Option( + "--type", + help="Type of libraries to list (github=symbols/footprints, cloud=3D models)", + ), + ] = "all", + verbose: Annotated[ + bool, + typer.Option("-v", "--verbose", help="Show more information about libraries"), + ] = False, +) -> None: """List all configured libraries in kilm. This shows all libraries stored in the kilm configuration file. @@ -66,13 +59,20 @@ def list(library_type, verbose): current_library = config.get_current_library() if not libraries: - click.echo("No libraries configured.") - click.echo("Use 'kilm init' to initialize a GitHub library.") - click.echo("Use 'kilm add-3d' to add a cloud-based 3D model library.") + console.print() + console.print( + Panel( + "[yellow]No libraries configured.[/yellow]\n\n" + "[cyan]Get Started:[/cyan]\n" + "• Initialize GitHub library: [blue]kilm init[/blue]\n" + "• Add 3D model library: [blue]kilm add-3d[/blue]\n" + "• Check status: [blue]kilm status[/blue]", + title="[bold yellow]⚠️ No Libraries[/bold yellow]", + border_style="yellow", + ) + ) return - click.echo("Configured Libraries:") - # Group libraries by type types = {"github": [], "cloud": []} for lib in libraries: @@ -80,127 +80,255 @@ def list(library_type, verbose): if lib_type in types: types[lib_type].append(lib) - # Print libraries grouped by type - if library_type in ["all", "github"] and types["github"]: - click.echo("\nGitHub Libraries (symbols, footprints, templates):") - for lib in types["github"]: - name = lib.get("name", "unnamed") - path = lib.get("path", "unknown") - path_obj = Path(path) + console.print() - # Mark current library - current_marker = "" - if path == current_library: - current_marker = " (current)" + # Display GitHub Libraries + if library_type in ["all", "github"] and types["github"]: + console.print( + "\n[bold cyan]GitHub Libraries[/bold cyan] [dim](symbols, footprints, templates)[/dim]" + ) + console.print() - if verbose: - click.echo(f" - {name}{current_marker}:") - click.echo(f" Path: {path}") + if verbose: + # Verbose mode: Individual panels for each library + for lib in types["github"]: + name = lib.get("name", "unnamed") + path = lib.get("path", "unknown") + path_obj = Path(path) - # Show metadata if available + # Get metadata metadata = read_github_metadata(path_obj) + + # Build content + content = f"[blue]Path:[/blue] {path}\n" + if metadata: - click.echo(f" Metadata: {GITHUB_METADATA_FILE} present") if "description" in metadata: - click.echo(f" Description: {metadata['description']}") + content += f"[green]Description:[/green] {metadata['description']}\n" if "version" in metadata: - click.echo(f" Version: {metadata['version']}") - if "env_var" in metadata and metadata["env_var"]: - click.echo( - f" Environment Variable: {metadata['env_var']}" + content += ( + f"[yellow]Version:[/yellow] {metadata['version']}\n" ) + if "env_var" in metadata and metadata["env_var"]: + content += f"[magenta]Environment Variable:[/magenta] {metadata['env_var']}\n" + + # Capabilities with clear labels if "capabilities" in metadata: caps = metadata["capabilities"] if isinstance(caps, dict): - click.echo( - " Capabilities: " - + f"symbols={'✓' if caps.get('symbols') else '✗'}, " - + f"footprints={'✓' if caps.get('footprints') else '✗'}, " - + f"templates={'✓' if caps.get('templates') else '✗'}" - ) + content += "[white]Features:[/white] " + features = [] + if caps.get("symbols"): + features.append("[green]✓ Symbols[/green]") + else: + features.append("[red]✗ Symbols[/red]") + if caps.get("footprints"): + features.append("[green]✓ Footprints[/green]") + else: + features.append("[red]✗ Footprints[/red]") + if caps.get("templates"): + features.append("[green]✓ Templates[/green]") + else: + features.append("[red]✗ Templates[/red]") + content += " | ".join(features) else: - click.echo(f" Metadata: No {GITHUB_METADATA_FILE} file") - - # Check for existence of key folders - folders = [] - if (path_obj / "symbols").exists(): - folders.append("symbols") - if (path_obj / "footprints").exists(): - folders.append("footprints") - if (path_obj / "templates").exists(): - folders.append("templates") - click.echo( - f" Folders: {', '.join(folders) if folders else 'none'}" + content += ( + f"[dim]No {GITHUB_METADATA_FILE} metadata file found[/dim]" + ) + + # Status indicator + status = ( + "[green]✓ CURRENT[/green]" + if path == current_library + else "[dim]Available[/dim]" ) - else: - click.echo(f" - {name}: {path}{current_marker}") + title = f"[bold cyan]{name}[/bold cyan] [{status}]" - if library_type in ["all", "cloud"] and types["cloud"]: - click.echo("\nCloud Libraries (3D models):") - for lib in types["cloud"]: - name = lib.get("name", "unnamed") - path = lib.get("path", "unknown") - path_obj = Path(path) + console.print( + Panel( + content, + title=title, + border_style="cyan" if path == current_library else "dim", + ) + ) + console.print() + else: + # Compact mode: Simple table + table = Table( + show_header=True, header_style="bold magenta", border_style="cyan" + ) - # Mark current library - current_marker = "" - if path == current_library: - current_marker = " (current)" + table.add_column("Library", style="cyan", no_wrap=True) + table.add_column("Status", justify="center", style="green", width=12) + table.add_column("Path", style="blue") - if verbose: - click.echo(f" - {name}{current_marker}:") - click.echo(f" Path: {path}") + for lib in types["github"]: + name = lib.get("name", "unnamed") + path = lib.get("path", "unknown") - # Show metadata if available + status = ( + "[green]✓ Current[/green]" + if path == current_library + else "[dim]Available[/dim]" + ) + + table.add_row(f"[bold]{name}[/bold]", status, path) + + console.print(table) + console.print() + + # Display Cloud Libraries + if library_type in ["all", "cloud"] and types["cloud"]: + console.print( + "\n[bold cyan]Cloud Libraries[/bold cyan] [dim](3D models)[/dim]" + ) + console.print() + + if verbose: + # Verbose mode: Individual panels for each library + for lib in types["cloud"]: + name = lib.get("name", "unnamed") + path = lib.get("path", "unknown") + path_obj = Path(path) + + # Get metadata metadata = read_cloud_metadata(path_obj) + + # Build content + content = f"[blue]Path:[/blue] {path}\n" + if metadata: - click.echo(f" Metadata: {CLOUD_METADATA_FILE} present") if "description" in metadata: - click.echo(f" Description: {metadata['description']}") + content += f"[green]Description:[/green] {metadata['description']}\n" if "version" in metadata: - click.echo(f" Version: {metadata['version']}") - if "env_var" in metadata and metadata["env_var"]: - click.echo( - f" Environment Variable: {metadata['env_var']}" + content += ( + f"[yellow]Version:[/yellow] {metadata['version']}\n" ) + if "env_var" in metadata and metadata["env_var"]: + content += f"[magenta]Environment Variable:[/magenta] {metadata['env_var']}\n" + + # Model count if "model_count" in metadata: - click.echo(f" 3D Models: {metadata['model_count']}") + content += ( + f"[white]3D Models:[/white] {metadata['model_count']}" + ) + else: + # Count models + count = 0 + for ext in [".step", ".stp", ".wrl", ".wings"]: + count += len(list(path_obj.glob(f"**/*{ext}"))) + content += f"[white]3D Models:[/white] {count} [dim](counted)[/dim]" else: - click.echo(f" Metadata: No {CLOUD_METADATA_FILE} file") - - # Count 3D model files if metadata not available or to verify - if not metadata or "model_count" not in metadata: - model_count = 0 + content += ( + f"[dim]No {CLOUD_METADATA_FILE} metadata file found[/dim]" + ) + # Still count models + count = 0 for ext in [".step", ".stp", ".wrl", ".wings"]: - model_count += len(list(path_obj.glob(f"**/*{ext}"))) - click.echo(f" 3D Models: {model_count} (counted)") - else: - click.echo(f" - {name}: {path}{current_marker}") + count += len(list(path_obj.glob(f"**/*{ext}"))) + content += ( + f"\n[white]3D Models:[/white] {count} [dim](counted)[/dim]" + ) + + # Status indicator + status = ( + "[green]✓ CURRENT[/green]" + if path == current_library + else "[dim]Available[/dim]" + ) + title = f"[bold cyan]{name}[/bold cyan] [{status}]" + + console.print( + Panel( + content, + title=title, + border_style="cyan" if path == current_library else "dim", + ) + ) + console.print() + else: + # Compact mode: Simple table + table = Table( + show_header=True, header_style="bold magenta", border_style="cyan" + ) + + table.add_column("Library", style="cyan", no_wrap=True) + table.add_column("Status", justify="center", style="green", width=12) + table.add_column("Path", style="blue") + + for lib in types["cloud"]: + name = lib.get("name", "unnamed") + path = lib.get("path", "unknown") + + status = ( + "[green]✓ Current[/green]" + if path == current_library + else "[dim]Available[/dim]" + ) + + table.add_row(f"[bold]{name}[/bold]", status, path) + + console.print(table) + console.print() + + # Add summary panel + total_libs = len(libraries) + github_count = len(types["github"]) + cloud_count = len(types["cloud"]) + + summary_content = f"[green]Total Libraries:[/green] {total_libs}\n" + summary_content += ( + f"[cyan]GitHub:[/cyan] {github_count} [blue]Cloud:[/blue] {cloud_count}" + ) + + console.print( + Panel( + summary_content, + title="[bold cyan]Summary[/bold cyan]", + border_style="cyan", + width=35, + ) + ) # Print helpful message if no libraries match the filter if library_type == "github" and not types["github"]: - click.echo("No GitHub libraries configured.") - click.echo("Use 'kilm init' to initialize a GitHub library.") + console.print( + Panel( + "[yellow]No GitHub libraries configured.[/yellow]\n\n" + "[cyan]Get Started:[/cyan]\n" + "• Initialize a GitHub library: [blue]kilm init[/blue]", + title="[bold yellow]⚠️ No GitHub Libraries[/bold yellow]", + border_style="yellow", + ) + ) elif library_type == "cloud" and not types["cloud"]: - click.echo("No cloud libraries configured.") - click.echo("Use 'kilm add-3d' to add a cloud-based 3D model library.") + console.print( + Panel( + "[yellow]No cloud libraries configured.[/yellow]\n\n" + "[cyan]Get Started:[/cyan]\n" + "• Add a 3D model library: [blue]kilm add-3d[/blue]", + title="[bold yellow]⚠️ No Cloud Libraries[/bold yellow]", + border_style="yellow", + ) + ) except Exception as e: - click.echo(f"Error listing configurations: {e}", err=True) + console.print(f"[red]Error listing configurations: {e}[/red]") sys.exit(1) -@config.command() -@click.argument("library_name", required=False) -@click.option( - "--type", - "library_type", - type=click.Choice(["github", "cloud"]), - default="github", - help="Type of library to set as default (github=symbols/footprints, cloud=3D models)", - show_default=True, -) -def set_default(library_name, library_type): +def set_default( + library_name: Annotated[ + Optional[str], typer.Argument(help="Name of library to set as default") + ] = None, + library_type: Annotated[ + str, + typer.Option( + "--type", + help="Type of library to set as default (github=symbols/footprints, cloud=3D models)", + ), + ] = "github", +) -> None: """Set a library as the default for operations. Sets the specified library as the default for future operations. @@ -234,11 +362,13 @@ def set_default(library_name, library_type): libraries = config.get_libraries(library_type) if not libraries: - click.echo(f"No {library_type} libraries configured.") + console.print(f"No {library_type} libraries configured.") if library_type == "github": - click.echo("Use 'kilm init' to initialize a GitHub library.") + console.print("Use 'kilm init' to initialize a GitHub library.") else: - click.echo("Use 'kilm add-3d' to add a cloud-based 3D model library.") + console.print( + "Use 'kilm add-3d' to add a cloud-based 3D model library." + ) sys.exit(1) # Get current library path @@ -246,7 +376,7 @@ def set_default(library_name, library_type): # If library name not provided, prompt for selection if not library_name: - click.echo(f"\nAvailable {library_type} libraries:") + console.print(f"\nAvailable {library_type} libraries:") # Show numbered list of libraries for i, lib in enumerate(libraries): @@ -258,12 +388,12 @@ def set_default(library_name, library_type): if path == current_library: current_marker = " (current)" - click.echo(f"{i + 1}. {name}{current_marker}") + console.print(f"{i + 1}. {name}{current_marker}") # Get selection while True: try: - selection = click.prompt( + selection = typer.prompt( "Select library (number)", type=int, default=1 ) if 1 <= selection <= len(libraries): @@ -272,11 +402,11 @@ def set_default(library_name, library_type): library_path = selected_lib.get("path") break else: - click.echo( + console.print( f"Please enter a number between 1 and {len(libraries)}" ) except ValueError: - click.echo("Please enter a valid number") + console.print("Please enter a valid number") else: # Find the library by name library_path = None @@ -286,41 +416,40 @@ def set_default(library_name, library_type): break if not library_path: - click.echo(f"No {library_type} library named '{library_name}' found.") - click.echo("Use 'kilm config list' to see available libraries.") + console.print( + f"No {library_type} library named '{library_name}' found." + ) + console.print("Use 'kilm config list' to see available libraries.") sys.exit(1) # Set as current library if library_path is None: - click.echo( - f"Error: Could not find path for library '{library_name}'", err=True + console.print( + f"[red]Error: Could not find path for library '{library_name}'[/red]" ) sys.exit(1) config.set_current_library(library_path) - click.echo(f"Set {library_type} library '{library_name}' as default.") - click.echo(f"Path: {library_path}") + console.print(f"Set {library_type} library '{library_name}' as default.") + console.print(f"Path: {library_path}") except Exception as e: - click.echo(f"Error setting default library: {e}", err=True) + console.print(f"[red]Error setting default library: {e}[/red]") sys.exit(1) -@config.command() -@click.argument("library_name", required=True) -@click.option( - "--type", - "library_type", - type=click.Choice(["github", "cloud", "all"]), - default="all", - help="Type of library to remove (all=remove from both types)", - show_default=True, -) -@click.option( - "--force", - is_flag=True, - help="Force removal without confirmation", -) -def remove(library_name, library_type, force): +def remove( + library_name: Annotated[str, typer.Argument(help="Name of library to remove")], + library_type: Annotated[ + str, + typer.Option( + "--type", + help="Type of library to remove (all=remove from both types)", + ), + ] = "all", + force: Annotated[ + bool, typer.Option("--force", help="Force removal without confirmation") + ] = False, +) -> None: """Remove a library from the configuration. Removes the specified library from the KiCad Library Manager configuration. @@ -359,10 +488,12 @@ def remove(library_name, library_type, force): if not matching_libraries: if library_type == "all": - click.echo(f"No library named '{library_name}' found.") + console.print(f"No library named '{library_name}' found.") else: - click.echo(f"No {library_type} library named '{library_name}' found.") - click.echo("Use 'kilm config list' to see available libraries.") + console.print( + f"No {library_type} library named '{library_name}' found." + ) + console.print("Use 'kilm config list' to see available libraries.") sys.exit(1) # Confirm removal @@ -370,16 +501,16 @@ def remove(library_name, library_type, force): for lib in matching_libraries: lib_type = lib.get("type", "unknown") lib_path = lib.get("path", "unknown") - click.echo( + console.print( f"Will remove {lib_type} library '{library_name}' from configuration." ) - click.echo(f"Path: {lib_path}") + console.print(f"Path: {lib_path}") if lib_path == current_library: - click.echo("Warning: This is the current default library.") + console.print("Warning: This is the current default library.") - if not click.confirm("Continue?"): - click.echo("Operation cancelled.") + if not typer.confirm("Continue?"): + console.print("Operation cancelled.") return # Remove libraries @@ -392,26 +523,21 @@ def remove(library_name, library_type, force): if removed_count > 0: if removed_count == 1: - click.echo(f"Removed library '{library_name}' from configuration.") + console.print(f"Removed library '{library_name}' from configuration.") else: - click.echo( + console.print( f"Removed {removed_count} instances of library '{library_name}' from configuration." ) # Check if we removed the current library current_library_new = config.get_current_library() if current_library and current_library != current_library_new: - click.echo( + console.print( "Note: Default library was changed as the previous default was removed." ) else: - click.echo("No libraries were removed.") + console.print("No libraries were removed.") except Exception as e: - click.echo(f"Error removing library: {e}", err=True) + console.print(f"[red]Error removing library: {e}[/red]") sys.exit(1) - - -# Register the config command -if __name__ == "__main__": - config() diff --git a/kicad_lib_manager/commands/init/__init__.py b/kicad_lib_manager/commands/init/__init__.py index 3ea0b37..30d926c 100644 --- a/kicad_lib_manager/commands/init/__init__.py +++ b/kicad_lib_manager/commands/init/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import init -__all__ = ["init"] +# Create Typer app and add the init command as callback +init_app = typer.Typer( + name="init", + help="Initialize library configuration", + rich_markup_mode="rich", + callback=init, + invoke_without_command=True, +) + +__all__ = ["init", "init_app"] diff --git a/kicad_lib_manager/commands/init/command.py b/kicad_lib_manager/commands/init/command.py index 408ccd7..e52cdcd 100644 --- a/kicad_lib_manager/commands/init/command.py +++ b/kicad_lib_manager/commands/init/command.py @@ -1,174 +1,124 @@ """ -Init command implementation for KiCad Library Manager. +Init command implementation for KiCad Library Manager (Typer version). Initializes the current directory as a KiCad library directory (symbols, footprints, templates). """ -import sys from pathlib import Path - -import click - -from ...config import Config -from ...utils.metadata import ( - GITHUB_METADATA_FILE, - generate_env_var_name, - get_default_github_metadata, - read_github_metadata, - write_github_metadata, -) - - -@click.command() -@click.option( - "--name", - help="Name for this library collection (automatic if not provided)", - default=None, -) -@click.option( - "--set-current", - is_flag=True, - default=True, - help="Set this as the current active library", - show_default=True, -) -@click.option( - "--description", - help="Description for this library collection", - default=None, -) -@click.option( - "--env-var", - help="Custom environment variable name for this library", - default=None, -) -@click.option( - "--force", - is_flag=True, - default=False, - help="Overwrite existing metadata file if present", - show_default=True, -) -@click.option( - "--no-env-var", - is_flag=True, - default=False, - help="Don't assign an environment variable to this library", - show_default=True, -) -def init(name, set_current, description, env_var, force, no_env_var): - """Initialize the current directory as a KiCad library collection. +from typing import Annotated, Optional + +import typer +from rich.console import Console +from rich.panel import Panel +from rich.prompt import Confirm + +from ...services.config_service import Config +from ...services.library_service import LibraryService + +console = Console() + + +def init( + name: Annotated[ + Optional[str], + typer.Option( + "--name", + help="Name for this library collection (automatic if not provided)", + ), + ] = None, + set_current: Annotated[ + bool, + typer.Option( + "--set-current/--no-set-current", + help="Set this as the current active library", + ), + ] = True, + description: Annotated[ + Optional[str], + typer.Option( + "--description", + help="Description for this library collection", + ), + ] = None, + env_var: Annotated[ + Optional[str], + typer.Option( + "--env-var", + help="Custom environment variable name for this library", + ), + ] = None, + force: Annotated[ + bool, + typer.Option( + "--force", + help="Overwrite existing metadata file if present", + ), + ] = False, + no_env_var: Annotated[ + bool, + typer.Option( + "--no-env-var", + help="Don't assign an environment variable to this library", + ), + ] = False, +) -> None: + """ + Initialize the current directory as a KiCad library collection. This command sets up the current directory as a KiCad library containing symbols, footprints, and templates. It creates the required folders if they don't exist and registers the library in the local configuration. - Each library can have its own unique environment variable name, which - will be used when setting up KiCad. This allows you to have multiple symbol/footprint - libraries and reference them individually. - - If a metadata file (kilm.yaml) already exists, information from it will be - used unless overridden by command line options. + [bold]Features:[/bold] + • Creates symbol, footprint, and template directories + • Generates metadata file with library information + • Assigns unique environment variable for KiCad integration + • Registers library in KiLM configuration - This is intended for GitHub-based libraries containing symbols and footprints, - not for 3D model libraries. + [bold]Note:[/bold] This is intended for GitHub-based libraries containing + symbols and footprints, not for 3D model libraries. """ current_dir = Path.cwd().resolve() - click.echo(f"Initializing KiCad library at: {current_dir}") - # Check for existing metadata - metadata = read_github_metadata(current_dir) + console.print( + Panel( + f"[bold blue]Initializing KiCad library[/bold blue]\n" + f"[cyan]Location:[/cyan] {current_dir}", + title="KiLM Library Initialization", + border_style="blue", + ) + ) + + # Use library service to initialize + library_service = LibraryService() + try: + metadata = library_service.initialize_library( + directory=current_dir, + name=name, + description=description, + env_var=env_var, + force=force, + no_env_var=no_env_var, + ) - if metadata and not force: - click.echo(f"Found existing metadata file ({GITHUB_METADATA_FILE}).") library_name = metadata.get("name") - library_description = metadata.get("description") library_env_var = metadata.get("env_var") - click.echo(f"Using existing name: {library_name}") - - # Show environment variable if present - if library_env_var and not no_env_var: - click.echo(f"Using existing environment variable: {library_env_var}") - - # Override with command line parameters if provided - if name: - library_name = name - click.echo(f"Overriding with provided name: {library_name}") - if description: - library_description = description - click.echo(f"Overriding with provided description: {library_description}") + # Get the directory status + existing_folders = [] + created_folders = [] + capabilities = metadata.get("capabilities", {}) - if env_var: - library_env_var = env_var - click.echo( - f"Overriding with provided environment variable: {library_env_var}" - ) - elif no_env_var: - library_env_var = None - click.echo("Disabling environment variable as requested") - - # Update metadata if command line parameters were provided - if name or description or env_var or no_env_var: - if library_name is not None: - metadata["name"] = library_name - if library_description is not None: - metadata["description"] = library_description - if library_env_var and not no_env_var: - metadata["env_var"] = library_env_var - else: - # Don't set env_var if not needed - pass - metadata["updated_with"] = "kilm" - write_github_metadata(current_dir, metadata) - click.echo("Updated metadata file with new information.") - else: - # Create a new metadata file - if metadata and force: - click.echo(f"Overwriting existing metadata file ({GITHUB_METADATA_FILE}).") - else: - click.echo(f"Creating new metadata file ({GITHUB_METADATA_FILE}).") - - # Generate metadata - metadata = get_default_github_metadata(current_dir) - - # Override with command line parameters if provided - if name: - metadata["name"] = name - # If name is provided but env_var isn't, regenerate the env_var based on the new name - if not env_var and not no_env_var: - metadata["env_var"] = generate_env_var_name(name, "KICAD_LIB") - - if description: - metadata["description"] = description - - if env_var: - metadata["env_var"] = env_var - elif no_env_var: - metadata["env_var"] = None - - # Write metadata file - write_github_metadata(current_dir, metadata) - click.echo("Metadata file created.") - - library_name = metadata["name"] - library_env_var = metadata.get("env_var") + for folder_type, exists in capabilities.items(): + if exists: + folder_path = current_dir / folder_type + if folder_path.exists(): + existing_folders.append(folder_type) + else: + created_folders.append(folder_type) - # Create library directory structure if folders don't exist - required_folders = ["symbols", "footprints", "templates"] - existing_folders = [] - created_folders = [] - - for folder in required_folders: - folder_path = current_dir / folder - if folder_path.exists(): - existing_folders.append(folder) - else: - try: - folder_path.mkdir(parents=True, exist_ok=True) - created_folders.append(folder) - except Exception as e: - click.echo(f"Error creating {folder} directory: {e}", err=True) - sys.exit(1) + except Exception as e: + console.print(f"[red]Error initializing library: {e}[/red]") + raise typer.Exit(1) from e # Create empty library_descriptions.yaml if it doesn't exist library_descriptions_file = current_dir / "library_descriptions.yaml" @@ -192,34 +142,34 @@ def init(name, set_current, description, env_var, force, no_env_var): """ with library_descriptions_file.open("w", encoding="utf-8") as f: f.write(template_content) - click.echo("Created library_descriptions.yaml template file.") + console.print( + "[green]Created library_descriptions.yaml template file.[/green]" + ) except Exception as e: - click.echo( - f"Warning: Could not create library_descriptions.yaml file: {e}", - err=True, + console.print( + f"[yellow]Warning: Could not create library_descriptions.yaml file: {e}[/yellow]" ) - # Update the metadata with current capabilities - updated_capabilities = { - "symbols": (current_dir / "symbols").exists(), - "footprints": (current_dir / "footprints").exists(), - "templates": (current_dir / "templates").exists(), - } - metadata["capabilities"] = updated_capabilities - write_github_metadata(current_dir, metadata) + # Metadata is already updated by the service # Report on folder status if existing_folders: - click.echo(f"Found existing folders: {', '.join(existing_folders)}") + console.print( + f"[blue]Found existing folders:[/blue] {', '.join(existing_folders)}" + ) if created_folders: - click.echo(f"Created new folders: {', '.join(created_folders)}") + console.print( + f"[green]Created new folders:[/green] {', '.join(created_folders)}" + ) # Verify if this looks like a KiCad library if not created_folders and not existing_folders: - click.echo("Warning: No library folders were found or created.") - if not click.confirm("Continue anyway?", default=True): - click.echo("Initialization cancelled.") - sys.exit(0) + console.print( + "[yellow]Warning: No library folders were found or created.[/yellow]" + ) + if not Confirm.ask("Continue anyway?", default=True): + console.print("[yellow]Initialization cancelled.[/yellow]") + raise typer.Exit(0) # Update the configuration try: @@ -231,22 +181,41 @@ def init(name, set_current, description, env_var, force, no_env_var): if set_current: config.set_current_library(str(current_dir)) - click.echo(f"Library '{safe_library_name}' initialized successfully!") - click.echo("Type: GitHub library (symbols, footprints, templates)") - click.echo(f"Path: {current_dir}") + # Create success panel + success_content = f"[bold green]Library '{safe_library_name}' initialized successfully![/bold green]\n\n" + success_content += ( + "[cyan]Type:[/cyan] GitHub library (symbols, footprints, templates)\n" + ) + success_content += f"[cyan]Path:[/cyan] {current_dir}\n" if library_env_var: - click.echo(f"Assigned environment variable: {library_env_var}") + success_content += f"[cyan]Environment Variable:[/cyan] {library_env_var}\n" if set_current: - click.echo("This is now your current active library.") - click.echo("kilm will use this library for all commands by default.") + success_content += ( + "\n[yellow]This is now your current active library.[/yellow]\n" + ) + success_content += ( + "[dim]KiLM will use this library for all commands by default.[/dim]" + ) + + console.print( + Panel( + success_content, + title="✅ Initialization Complete", + border_style="green", + ) + ) # Add a hint for adding 3D models - click.echo( - "\nTo add a 3D models directory (typically stored in the cloud), use:" + console.print("\n[bold]Next Steps:[/bold]") + console.print( + "To add a 3D models directory (typically stored in the cloud), use:" + ) + console.print( + "[dim] kilm add-3d --name my-3d-models --directory /path/to/3d/models[/dim]" ) - click.echo(" kilm add-3d --name my-3d-models --directory /path/to/3d/models") + except Exception as e: - click.echo(f"Error initializing library: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error initializing library: {e}[/red]") + raise typer.Exit(1) from e diff --git a/kicad_lib_manager/commands/list_libraries/__init__.py b/kicad_lib_manager/commands/list_libraries/__init__.py index df7abab..a05f940 100644 --- a/kicad_lib_manager/commands/list_libraries/__init__.py +++ b/kicad_lib_manager/commands/list_libraries/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import list_cmd -__all__ = ["list_cmd"] +# Create Typer app and add the list command as callback +list_app = typer.Typer( + name="list", + help="List available KiCad libraries", + rich_markup_mode="rich", + callback=list_cmd, + invoke_without_command=True, +) + +__all__ = ["list_cmd", "list_app"] diff --git a/kicad_lib_manager/commands/list_libraries/command.py b/kicad_lib_manager/commands/list_libraries/command.py index 89ad48d..334c181 100644 --- a/kicad_lib_manager/commands/list_libraries/command.py +++ b/kicad_lib_manager/commands/list_libraries/command.py @@ -1,49 +1,101 @@ """ -List command implementation for KiCad Library Manager. +List command implementation for KiCad Library Manager (Typer version). """ -import sys +from pathlib import Path +from typing import Annotated, Optional -import click +import typer +from rich.console import Console +from rich.table import Table -from ...library_manager import list_libraries +from ...services.library_service import LibraryService from ...utils.env_vars import expand_user_path, find_environment_variables +console = Console() -@click.command() -@click.option( - "--kicad-lib-dir", - envvar="KICAD_USER_LIB", - help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", -) -def list_cmd(kicad_lib_dir): - """List available libraries in the specified directory""" + +def list_cmd( + kicad_lib_dir: Annotated[ + Optional[str], + typer.Option( + "--kicad-lib-dir", + help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", + envvar="KICAD_USER_LIB", + ), + ] = None, +) -> None: + """ + List available libraries in the specified directory. + + This command scans the KiCad library directory for symbol (.kicad_sym) and + footprint (.pretty) libraries and displays them in organized tables. + """ # Find environment variables if not provided if not kicad_lib_dir: kicad_lib_dir = find_environment_variables("KICAD_USER_LIB") if not kicad_lib_dir: - click.echo("Error: KICAD_USER_LIB not set and not provided", err=True) - sys.exit(1) + console.print("[red]Error: KICAD_USER_LIB not set and not provided[/red]") + raise typer.Exit(1) # Expand user home directory if needed kicad_lib_dir = expand_user_path(kicad_lib_dir) try: - symbols, footprints = list_libraries(kicad_lib_dir) + library_service = LibraryService() + symbols, footprints = library_service.list_libraries(Path(kicad_lib_dir)) + + console.print(f"[blue]Scanning library directory:[/blue] {kicad_lib_dir}\n") + # Display symbol libraries in a table if symbols: - click.echo("\nAvailable Symbol Libraries:") + symbol_table = Table( + title="Available Symbol Libraries", + show_header=True, + header_style="bold cyan", + ) + symbol_table.add_column("Library Name", style="cyan", no_wrap=True) + symbol_table.add_column("Type", style="blue") + for symbol in sorted(symbols): - click.echo(f" - {symbol}") + symbol_table.add_row(symbol, ".kicad_sym") + + console.print(symbol_table) else: - click.echo("No symbol libraries found") + console.print("[yellow]No symbol libraries found[/yellow]") + + console.print() # Empty line for spacing + # Display footprint libraries in a table if footprints: - click.echo("\nAvailable Footprint Libraries:") + footprint_table = Table( + title="Available Footprint Libraries", + show_header=True, + header_style="bold magenta", + ) + footprint_table.add_column("Library Name", style="magenta", no_wrap=True) + footprint_table.add_column("Type", style="blue") + for footprint in sorted(footprints): - click.echo(f" - {footprint}") + footprint_table.add_row(footprint, ".pretty") + + console.print(footprint_table) else: - click.echo("No footprint libraries found") + console.print("[yellow]No footprint libraries found[/yellow]") + + # Summary information + total_libs = len(symbols) + len(footprints) + if total_libs > 0: + console.print( + f"\n[green]Found {total_libs} libraries total[/green] " + f"([cyan]{len(symbols)} symbol[/cyan], " + f"[magenta]{len(footprints)} footprint[/magenta])" + ) + else: + console.print( + "[yellow]No libraries found in the specified directory[/yellow]" + ) + except Exception as e: - click.echo(f"Error listing libraries: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error listing libraries: {e}[/red]") + raise typer.Exit(1) from e diff --git a/kicad_lib_manager/commands/pin/__init__.py b/kicad_lib_manager/commands/pin/__init__.py index 21044cf..24e32dc 100644 --- a/kicad_lib_manager/commands/pin/__init__.py +++ b/kicad_lib_manager/commands/pin/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import pin -__all__ = ["pin"] +# Create Typer app and add the pin command as callback +pin_app = typer.Typer( + name="pin", + help="Pin favorite libraries", + rich_markup_mode="rich", + callback=pin, + invoke_without_command=True, +) + +__all__ = ["pin", "pin_app"] diff --git a/kicad_lib_manager/commands/pin/command.py b/kicad_lib_manager/commands/pin/command.py index ea7f030..538a495 100644 --- a/kicad_lib_manager/commands/pin/command.py +++ b/kicad_lib_manager/commands/pin/command.py @@ -1,124 +1,163 @@ """ -Pin command implementation for KiCad Library Manager. +Pin command implementation for KiCad Library Manager (Typer version). """ -import sys +from pathlib import Path +from typing import Annotated, Optional -import click +import typer +from rich.console import Console +from rich.panel import Panel +from rich.table import Table -from ...library_manager import find_kicad_config, list_libraries +from ...services.kicad_service import KiCadService +from ...services.library_service import LibraryService from ...utils.env_vars import ( expand_user_path, find_environment_variables, update_pinned_libraries, ) +console = Console() + + +def pin( + kicad_lib_dir: Annotated[ + Optional[str], + typer.Option( + "--kicad-lib-dir", + envvar="KICAD_USER_LIB", + help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", + ), + ] = None, + symbols: Annotated[ + Optional[list[str]], + typer.Option( + "--symbols", + "-s", + help="Symbol libraries to pin (can be specified multiple times)", + ), + ] = None, + footprints: Annotated[ + Optional[list[str]], + typer.Option( + "--footprints", + "-f", + help="Footprint libraries to pin (can be specified multiple times)", + ), + ] = None, + all_libs: Annotated[ + bool, + typer.Option( + "--all/--selected", + help="Pin all available libraries or only selected ones", + ), + ] = True, + dry_run: Annotated[ + bool, + typer.Option( + "--dry-run", + help="Show what would be done without making changes", + ), + ] = False, + max_backups: Annotated[ + int, + typer.Option( + "--max-backups", + help="Maximum number of backups to keep", + ), + ] = 5, + verbose: Annotated[ + bool, + typer.Option( + "--verbose", + "-v", + help="Show verbose output for debugging", + ), + ] = False, +) -> None: + """ + Pin libraries in KiCad for quick access. + + This command pins libraries in KiCad's interface for quick access. + You can pin specific symbol and footprint libraries or all available libraries. + """ + # Initialize default values for mutable types + if symbols is None: + symbols = [] + if footprints is None: + footprints = [] -@click.command() -@click.option( - "--kicad-lib-dir", - envvar="KICAD_USER_LIB", - help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", -) -@click.option( - "--symbols", - "-s", - multiple=True, - help="Symbol libraries to pin (can be specified multiple times)", -) -@click.option( - "--footprints", - "-f", - multiple=True, - help="Footprint libraries to pin (can be specified multiple times)", -) -@click.option( - "--all/--selected", - default=True, - show_default=True, - help="Pin all available libraries or only selected ones", -) -@click.option( - "--dry-run", - is_flag=True, - help="Show what would be done without making changes", -) -@click.option( - "--max-backups", - default=5, - show_default=True, - help="Maximum number of backups to keep", -) -@click.option( - "--verbose", - "-v", - is_flag=True, - help="Show verbose output for debugging", -) -def pin(kicad_lib_dir, symbols, footprints, all, dry_run, max_backups, verbose): - """Pin libraries in KiCad for quick access""" # Find environment variables if not provided if not kicad_lib_dir: kicad_lib_dir = find_environment_variables("KICAD_USER_LIB") if not kicad_lib_dir: - click.echo("Error: KICAD_USER_LIB not set and not provided", err=True) - sys.exit(1) + console.print("[red]Error: KICAD_USER_LIB not set and not provided[/red]") + raise typer.Exit(1) # Expand user home directory if needed kicad_lib_dir = expand_user_path(kicad_lib_dir) if verbose: - click.echo(f"Using KiCad library directory: {kicad_lib_dir}") + console.print(f"[blue]Using KiCad library directory:[/blue] {kicad_lib_dir}") + + # Initialize services + library_service = LibraryService() + kicad_service = KiCadService() # Find KiCad configuration try: - kicad_config = find_kicad_config() + kicad_config = kicad_service.find_kicad_config_dir() if verbose: - click.echo(f"Found KiCad configuration at: {kicad_config}") + console.print(f"[blue]Found KiCad configuration at:[/blue] {kicad_config}") except Exception as e: - click.echo(f"Error finding KiCad configuration: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error finding KiCad configuration: {e}[/red]") + raise typer.Exit(1) from e # If --all is specified, get all libraries from the directory - if all and not symbols and not footprints: + if all_libs and not symbols and not footprints: try: - symbol_libs, footprint_libs = list_libraries(kicad_lib_dir) - symbols = symbol_libs - footprints = footprint_libs + symbol_libs, footprint_libs = library_service.list_libraries( + Path(kicad_lib_dir) + ) + symbols = list(symbol_libs) + footprints = list(footprint_libs) if verbose: - click.echo( - f"Found {len(symbols)} symbol libraries and {len(footprints)} footprint libraries" + console.print( + f"[green]Found {len(symbols)} symbol libraries and {len(footprints)} footprint libraries[/green]" ) except Exception as e: - click.echo(f"Error listing libraries: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error listing libraries: {e}[/red]") + raise typer.Exit(1) from e - # Convert tuples to lists if needed - if isinstance(symbols, tuple): - symbols = list(symbols) - if isinstance(footprints, tuple): - footprints = list(footprints) + # Ensure we have lists (Typer should already provide lists) + if not isinstance(symbols, list): + symbols = list(symbols) if symbols else [] + if not isinstance(footprints, list): + footprints = list(footprints) if footprints else [] # Validate that libraries exist - if not all and (symbols or footprints): + if not all_libs and (symbols or footprints): try: - available_symbols, available_footprints = list_libraries(kicad_lib_dir) + available_symbols, available_footprints = library_service.list_libraries( + Path(kicad_lib_dir) + ) # Check symbols for symbol in symbols: if symbol not in available_symbols: - click.echo( - f"Warning: Symbol library '{symbol}' not found", err=True + console.print( + f"[yellow]Warning: Symbol library '{symbol}' not found[/yellow]" ) # Check footprints for footprint in footprints: if footprint not in available_footprints: - click.echo( - f"Warning: Footprint library '{footprint}' not found", err=True + console.print( + f"[yellow]Warning: Footprint library '{footprint}' not found[/yellow]" ) except Exception as e: - click.echo(f"Error validating libraries: {e}", err=True) + console.print(f"[yellow]Error validating libraries: {e}[/yellow]") # Continue anyway, in case the libraries are configured but not in the directory try: @@ -132,28 +171,39 @@ def pin(kicad_lib_dir, symbols, footprints, all, dry_run, max_backups, verbose): if changes_needed: if dry_run: - click.echo( - f"Would pin {len(symbols)} symbol and {len(footprints)} footprint libraries in KiCad" + console.print( + f"[yellow]Would pin {len(symbols)} symbol and {len(footprints)} footprint libraries in KiCad[/yellow]" ) else: - click.echo( - f"Pinned {len(symbols)} symbol and {len(footprints)} footprint libraries in KiCad" + success_msg = f"[green]Pinned {len(symbols)} symbol and {len(footprints)} footprint libraries in KiCad[/green]" + console.print( + Panel( + f"{success_msg}\n\n" + f"[blue]• Created backup of kicad_common.json[/blue]\n" + f"[yellow]• Restart KiCad for changes to take effect[/yellow]", + title="✅ Libraries Pinned", + border_style="green", + ) ) - click.echo("Created backup of kicad_common.json") - click.echo("Restart KiCad for changes to take effect") else: - click.echo("No changes needed, libraries already pinned in KiCad") + console.print( + "[blue]No changes needed, libraries already pinned in KiCad[/blue]" + ) if verbose: if symbols: - click.echo("\nPinned Symbol Libraries:") + table = Table(title="Pinned Symbol Libraries") + table.add_column("Library", style="cyan") for symbol in sorted(symbols): - click.echo(f" - {symbol}") + table.add_row(symbol) + console.print(table) if footprints: - click.echo("\nPinned Footprint Libraries:") + table = Table(title="Pinned Footprint Libraries") + table.add_column("Library", style="magenta") for footprint in sorted(footprints): - click.echo(f" - {footprint}") + table.add_row(footprint) + console.print(table) except Exception as e: - click.echo(f"Error pinning libraries: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error pinning libraries: {e}[/red]") + raise typer.Exit(1) from e diff --git a/kicad_lib_manager/commands/setup/__init__.py b/kicad_lib_manager/commands/setup/__init__.py index 9142379..f409e05 100644 --- a/kicad_lib_manager/commands/setup/__init__.py +++ b/kicad_lib_manager/commands/setup/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import setup -__all__ = ["setup"] +# Create Typer app and add the setup command as callback +setup_app = typer.Typer( + name="setup", + help="Configure KiCad to use libraries", + rich_markup_mode="rich", + callback=setup, + invoke_without_command=True, +) + +__all__ = ["setup", "setup_app"] diff --git a/kicad_lib_manager/commands/setup/command.py b/kicad_lib_manager/commands/setup/command.py index f6b0715..fa91348 100644 --- a/kicad_lib_manager/commands/setup/command.py +++ b/kicad_lib_manager/commands/setup/command.py @@ -1,17 +1,18 @@ """ -Setup command implementation for KiCad Library Manager. +Setup command implementation for KiCad Library Manager (Typer version). """ import re -import sys from pathlib import Path -from typing import Dict, List +from typing import Optional -import click +import typer +from rich.console import Console +from rich.panel import Panel +from rich.table import Table -# TODO: Use full path (kicad_lib_manager...) -from ...config import Config, LibraryDict -from ...library_manager import add_libraries, find_kicad_config +from ...services.config_service import Config, LibraryDict +from ...services.library_service import LibraryService from ...utils.backup import create_backup from ...utils.env_vars import ( expand_user_path, @@ -22,6 +23,8 @@ from ...utils.file_ops import list_libraries from ...utils.metadata import read_cloud_metadata, read_github_metadata +console = Console() + def fix_invalid_uris( kicad_config: Path, @@ -74,65 +77,49 @@ def fix_invalid_uris( return changes_made -@click.command() -@click.option( - "--kicad-lib-dir", - envvar="KICAD_USER_LIB", - help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", -) -@click.option( - "--kicad-3d-dir", - envvar="KICAD_3D_LIB", - help="KiCad 3D models directory (uses KICAD_3D_LIB env var if not specified)", -) -@click.option( - "--threed-lib-dirs", - help="Names of 3D model libraries to use (comma-separated, uses all if not specified)", -) -@click.option( - "--symbol-lib-dirs", - help="Names of symbol libraries to use (comma-separated, uses current if not specified)", -) -@click.option( - "--all-libraries", - is_flag=True, - default=False, - help="Set up all configured libraries (both symbols and 3D models)", -) -@click.option( - "--max-backups", - default=5, - show_default=True, - help="Maximum number of backups to keep", -) -@click.option( - "--dry-run", - is_flag=True, - help="Show what would be done without making changes", -) -@click.option( - "--pin-libraries/--no-pin-libraries", - default=True, - show_default=True, - help="Add libraries to KiCad pinned libraries for quick access", -) -@click.option( - "--verbose", - "-v", - is_flag=True, - help="Show more information for debugging", -) def setup( - kicad_lib_dir, - kicad_3d_dir, - threed_lib_dirs, - symbol_lib_dirs, - all_libraries, - max_backups, - dry_run, - pin_libraries, - verbose, -): + kicad_lib_dir: Optional[str] = typer.Option( + None, + "--kicad-lib-dir", + envvar="KICAD_USER_LIB", + help="KiCad library directory (uses KICAD_USER_LIB env var if not specified)", + ), + kicad_3d_dir: Optional[str] = typer.Option( + None, + "--kicad-3d-dir", + envvar="KICAD_3D_LIB", + help="KiCad 3D models directory (uses KICAD_3D_LIB env var if not specified)", + ), + threed_lib_dirs: Optional[str] = typer.Option( + None, + "--threed-lib-dirs", + help="Names of 3D model libraries to use (comma-separated, uses all if not specified)", + ), + symbol_lib_dirs: Optional[str] = typer.Option( + None, + "--symbol-lib-dirs", + help="Names of symbol libraries to use (comma-separated, uses current if not specified)", + ), + all_libraries: bool = typer.Option( + False, + "--all-libraries", + help="Set up all configured libraries (both symbols and 3D models)", + ), + max_backups: int = typer.Option( + 5, "--max-backups", help="Maximum number of backups to keep" + ), + dry_run: bool = typer.Option( + False, "--dry-run", help="Show what would be done without making changes" + ), + pin_libraries: bool = typer.Option( + True, + "--pin-libraries/--no-pin-libraries", + help="Add libraries to KiCad pinned libraries for quick access", + ), + verbose: bool = typer.Option( + False, "--verbose", "-v", help="Show more information for debugging" + ), +) -> None: """Configure KiCad to use libraries in the specified directory This command sets up KiCad to use your configured libraries. It will: @@ -150,30 +137,34 @@ def setup( if kicad_lib_dir: cmd_line_lib_paths["symbols"] = kicad_lib_dir if verbose: - click.echo(f"Symbol library specified on command line: {kicad_lib_dir}") + console.print( + f"Symbol library specified on command line: [blue]{kicad_lib_dir}[/blue]" + ) if kicad_3d_dir: cmd_line_lib_paths["3d"] = kicad_3d_dir if verbose: - click.echo(f"3D model library specified on command line: {kicad_3d_dir}") + console.print( + f"3D model library specified on command line: [blue]{kicad_3d_dir}[/blue]" + ) # Split library names if provided threed_lib_names = None if threed_lib_dirs: threed_lib_names = [name.strip() for name in threed_lib_dirs.split(",")] if verbose: - click.echo(f"Requested 3D model libraries: {threed_lib_names}") + console.print(f"Requested 3D model libraries: {threed_lib_names}") symbol_lib_names = None if symbol_lib_dirs: symbol_lib_names = [name.strip() for name in symbol_lib_dirs.split(",")] if verbose: - click.echo(f"Requested symbol libraries: {symbol_lib_names}") + console.print(f"Requested symbol libraries: {symbol_lib_names}") # Check Config file for library paths - config_lib_paths: Dict[str, str] = {} - config_3d_libs: List[LibraryDict] = [] - config_symbol_libs: List[LibraryDict] = [] + config_lib_paths: dict[str, str] = {} + config_3d_libs: list[LibraryDict] = [] + config_symbol_libs: list[LibraryDict] = [] config_obj = None try: @@ -182,18 +173,18 @@ def setup( # Display configuration file location if verbose if verbose: config_file = config_obj._get_config_file() - click.echo(f"Looking for configuration in: {config_file}") + console.print(f"Looking for configuration in: {config_file}") if config_file.exists(): - click.echo("Configuration file exists") + console.print("Configuration file exists") else: - click.echo("Configuration file does not exist") + console.print("Configuration file does not exist") # Get all configured libraries all_symbol_libs = config_obj.get_libraries("github") all_3d_libs = config_obj.get_libraries("cloud") if verbose: - click.echo( + console.print( f"Found {len(all_symbol_libs)} symbol libraries and {len(all_3d_libs)} 3D model libraries in config" ) @@ -238,11 +229,20 @@ def setup( # Print what we're setting up if config_symbol_libs: - click.echo("\nSetting up symbol libraries:") + console.print() + table = Table( + title="[bold cyan]Setting up symbol libraries[/bold cyan]", + show_header=True, + header_style="bold magenta", + ) + table.add_column("Library", style="cyan", no_wrap=True) + table.add_column("Path", style="blue") + table.add_column("Environment Variable", style="green") + for lib in config_symbol_libs: lib_name = lib.get("name", "unnamed") lib_path = lib.get("path", "unknown") - click.echo(f" - {lib_name}: {lib_path}") + env_var_display = "None" # Read metadata to get environment variable name try: @@ -252,14 +252,15 @@ def setup( if env_var and isinstance(env_var, str): # Store all GitHub libraries with their env vars config_lib_paths[env_var] = lib_path - click.echo(f" Using environment variable: {env_var}") + env_var_display = f"[green]{env_var}[/green]" else: - click.echo(" No environment variable configured") + env_var_display = "[yellow]Not configured[/yellow]" else: - click.echo(" No metadata or environment variable found") + env_var_display = "[yellow]No metadata[/yellow]" except Exception as e: - if verbose: - click.echo(f" Error reading metadata: {e}") + env_var_display = ( + f"[red]Error: {e}[/red]" if verbose else "[red]Error[/red]" + ) # If we're using the first symbol library as the main library if not kicad_lib_dir and lib == config_symbol_libs[0]: @@ -268,12 +269,25 @@ def setup( if "KICAD_USER_LIB" not in config_lib_paths: config_lib_paths["KICAD_USER_LIB"] = lib_path + table.add_row(f"[bold]{lib_name}[/bold]", lib_path, env_var_display) + + console.print(table) + if config_3d_libs: - click.echo("\nSetting up 3D model libraries:") + console.print() + table = Table( + title="[bold cyan]Setting up 3D model libraries[/bold cyan]", + show_header=True, + header_style="bold magenta", + ) + table.add_column("Library", style="cyan", no_wrap=True) + table.add_column("Path", style="blue") + table.add_column("Environment Variable", style="green") + for lib in config_3d_libs: lib_name = lib.get("name", "unnamed") lib_path = lib.get("path", "unknown") - click.echo(f" - {lib_name}: {lib_path}") + env_var_display = "None" # Read metadata to get environment variable name try: @@ -283,26 +297,31 @@ def setup( if env_var and isinstance(env_var, str): # Store all 3D libraries with their env vars config_lib_paths[env_var] = lib_path - click.echo(f" Using environment variable: {env_var}") + env_var_display = f"[green]{env_var}[/green]" else: - click.echo(" No environment variable configured") + env_var_display = "[yellow]Not configured[/yellow]" else: - click.echo(" No metadata or environment variable found") + env_var_display = "[yellow]No metadata[/yellow]" except Exception as e: - if verbose: - click.echo(f" Error reading metadata: {e}") + env_var_display = ( + f"[red]Error: {e}[/red]" if verbose else "[red]Error[/red]" + ) # Use the first 3D library as the default if not specified if not kicad_3d_dir and lib == config_3d_libs[0]: kicad_3d_dir = lib_path + table.add_row(f"[bold]{lib_name}[/bold]", lib_path, env_var_display) + + console.print(table) + except Exception as e: # If there's any issue with config, continue with environment variables if verbose: - click.echo(f"Error reading from config: {e}") + console.print(f"Error reading from config: {e}") import traceback - click.echo(traceback.format_exc()) + console.print(traceback.format_exc()) # Fall back to environment variables if still not found env_lib_paths = {} @@ -311,75 +330,96 @@ def setup( if env_var: kicad_lib_dir = env_var env_lib_paths["KICAD_USER_LIB"] = env_var - click.echo( - f"Using KiCad library from environment variable: {kicad_lib_dir}" + console.print( + f"[green]Using KiCad library from environment variable:[/green] [blue]{kicad_lib_dir}[/blue]" ) else: - click.echo("Error: KICAD_USER_LIB not set and not provided", err=True) - click.echo( - "Consider initializing a library with 'kilm init' first.", err=True + console.print("[red]Error: KICAD_USER_LIB not set and not provided[/red]") + console.print( + "[yellow]Consider initializing a library with 'kilm init' first.[/yellow]" ) - sys.exit(1) + raise typer.Exit(1) if not kicad_3d_dir: env_var = find_environment_variables("KICAD_3D_LIB") if env_var: kicad_3d_dir = env_var env_lib_paths["KICAD_3D_LIB"] = env_var - click.echo( - f"Using 3D model library from environment variable: {kicad_3d_dir}" + console.print( + f"[green]Using 3D model library from environment variable:[/green] [blue]{kicad_3d_dir}[/blue]" ) else: - click.echo( - "Warning: KICAD_3D_LIB not set, 3D models might not work correctly", - err=True, + console.print( + "[yellow]Warning: KICAD_3D_LIB not set, 3D models might not work correctly[/yellow]" ) - click.echo( - "Consider adding a 3D model directory with 'kilm add-3d'", err=True + console.print( + "[yellow]Consider adding a 3D model directory with 'kilm add-3d'[/yellow]" ) # Show summary of where libraries are coming from if verbose: - click.echo("\nSummary of library sources:") + console.print("\nSummary of library sources:") if cmd_line_lib_paths: - click.echo(" From command line:") + console.print(" From command line:") for lib_type, path in cmd_line_lib_paths.items(): - click.echo(f" - {lib_type}: {path}") + console.print(f" - {lib_type}: {path}") if config_lib_paths: - click.echo(" From config file:") + console.print(" From config file:") for lib_type, path in config_lib_paths.items(): - click.echo(f" - {lib_type}: {path}") + console.print(f" - {lib_type}: {path}") if env_lib_paths: - click.echo(" From environment variables:") + console.print(" From environment variables:") for lib_type, path in env_lib_paths.items(): - click.echo(f" - {lib_type}: {path}") + console.print(f" - {lib_type}: {path}") # Expand user home directory if needed kicad_lib_dir = expand_user_path(kicad_lib_dir) if kicad_3d_dir: kicad_3d_dir = expand_user_path(kicad_3d_dir) - click.echo(f"\nUsing KiCad symbol library directory: {kicad_lib_dir}") + # Create configuration summary panel + config_content = ( + f"[green]Symbol library directory:[/green] [blue]{kicad_lib_dir}[/blue]\n" + ) if kicad_3d_dir: - click.echo(f"Using KiCad main 3D models directory: {kicad_3d_dir}") + config_content += ( + f"[green]3D models directory:[/green]\n[blue]{kicad_3d_dir}[/blue]" + ) + else: + config_content += "[yellow]3D models directory: Not configured[/yellow]" + + console.print() + console.print( + Panel( + config_content, + title="[bold cyan]Configuration Summary[/bold cyan]", + border_style="cyan", + ) + ) # Find KiCad configuration try: - kicad_config = find_kicad_config() - click.echo(f"Found KiCad configuration at: {kicad_config}") + kicad_config = LibraryService.find_kicad_config() + console.print( + f"[green]Found KiCad configuration at:[/green] [blue]{kicad_config}[/blue]" + ) # Fix any invalid URIs in existing library entries uri_changes = fix_invalid_uris(kicad_config, True, max_backups, dry_run) if uri_changes: if dry_run: - click.echo("Would fix invalid library URIs in KiCad configuration") + console.print( + "[yellow]Would fix invalid library URIs in KiCad configuration[/yellow]" + ) else: - click.echo("Fixed invalid library URIs in KiCad configuration") + console.print( + "[green]Fixed invalid library URIs in KiCad configuration[/green]" + ) except Exception as e: - click.echo(f"Error finding KiCad configuration: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error finding KiCad configuration: {e}[/red]") + raise typer.Exit(1) from e # Prepare environment variables dictionary env_vars = {} @@ -406,21 +446,27 @@ def setup( ) if env_changes_needed: if dry_run: - click.echo("Would update environment variables in KiCad configuration") + console.print( + "[yellow]Would update environment variables in KiCad configuration[/yellow]" + ) else: - click.echo("Updated environment variables in KiCad configuration") - click.echo("Created backup of kicad_common.json") + console.print( + "[green]Updated environment variables in KiCad configuration[/green]" + ) + console.print("[blue]Created backup of kicad_common.json[/blue]") # Show all environment variables that were set - click.echo("\nEnvironment variables set in KiCad:") + console.print( + "\n[bold cyan]Environment variables set in KiCad:[/bold cyan]" + ) for var_name, value in env_vars.items(): - click.echo(f" {var_name} = {value}") + console.print(f" [cyan]{var_name}[/cyan] = [blue]{value}[/blue]") else: - click.echo( - "Environment variables already up to date in KiCad configuration" + console.print( + "[blue]Environment variables already up to date in KiCad configuration[/blue]" ) except Exception as e: - click.echo(f"Error updating environment variables: {e}", err=True) + console.print(f"[red]Error updating environment variables: {e}[/red]") # Continue with the rest of the setup, but don't set env_changes_needed to True # Add libraries @@ -436,7 +482,7 @@ def setup( three_d_dirs["KICAD_3D_LIB"] = kicad_3d_dir # Call add_libraries with the main library and all 3D libraries - added_libraries, changes_needed = add_libraries( + added_libraries, changes_needed = LibraryService.add_libraries( kicad_lib_dir, kicad_config, kicad_3d_dir=kicad_3d_dir, @@ -451,23 +497,23 @@ def setup( if sym_table.exists(): create_backup(sym_table, max_backups) - click.echo("Created backup of symbol library table") + console.print("[blue]Created backup of symbol library table[/blue]") if fp_table.exists(): create_backup(fp_table, max_backups) - click.echo("Created backup of footprint library table") + console.print("[blue]Created backup of footprint library table[/blue]") if added_libraries: if dry_run: - click.echo( - f"Would add {len(added_libraries)} libraries to KiCad configuration" + console.print( + f"[yellow]Would add {len(added_libraries)} libraries to KiCad configuration[/yellow]" ) else: - click.echo( - f"Added {len(added_libraries)} libraries to KiCad configuration" + console.print( + f"[green]Added {len(added_libraries)} libraries to KiCad configuration[/green]" ) else: - click.echo("No new libraries to add") + console.print("[blue]No new libraries to add[/blue]") # Pin libraries if requested pinned_changes_needed = False @@ -483,11 +529,11 @@ def setup( footprint_libs = existing_footprints if verbose: - click.echo( + console.print( f"Found {len(symbol_libs)} symbol libraries and {len(footprint_libs)} footprint libraries to pin" ) except Exception as e: - click.echo(f"Error listing libraries to pin: {e}", err=True) + console.print(f"[red]Error listing libraries to pin: {e}[/red]") try: pinned_changes_needed = update_pinned_libraries( @@ -499,29 +545,36 @@ def setup( if pinned_changes_needed: if dry_run: - click.echo( - f"Would pin {len(symbol_libs)} symbol and {len(footprint_libs)} footprint libraries in KiCad" + console.print( + f"[yellow]Would pin {len(symbol_libs)} symbol and {len(footprint_libs)} footprint libraries in KiCad[/yellow]" ) else: - click.echo( - f"Pinned {len(symbol_libs)} symbol and {len(footprint_libs)} footprint libraries in KiCad" + console.print( + f"[green]Pinned {len(symbol_libs)} symbol and {len(footprint_libs)} footprint libraries in KiCad[/green]" ) else: - click.echo("All libraries already pinned in KiCad") + console.print("[blue]All libraries already pinned in KiCad[/blue]") except Exception as e: - click.echo(f"Error pinning libraries: {e}", err=True) + console.print(f"[red]Error pinning libraries: {e}[/red]") if not changes_needed and not env_changes_needed and not pinned_changes_needed: - click.echo("No changes needed, configuration is up to date") + console.print("[blue]No changes needed, configuration is up to date[/blue]") elif dry_run: - click.echo("Dry run: No changes were made") + console.print("[yellow]Dry run: No changes were made[/yellow]") except Exception as e: - click.echo(f"Error adding libraries: {e}", err=True) + console.print(f"[red]Error adding libraries: {e}[/red]") if verbose: import traceback - click.echo(traceback.format_exc()) - sys.exit(1) + console.print(traceback.format_exc()) + raise typer.Exit(1) from None if not dry_run and (changes_needed or env_changes_needed or pinned_changes_needed): - click.echo("Setup complete! Restart KiCad for changes to take effect.") + console.print() + console.print( + Panel( + "[bold green]Setup complete! Restart KiCad for changes to take effect.[/bold green]", + title="[bold green]✅ Success[/bold green]", + border_style="green", + ) + ) diff --git a/kicad_lib_manager/commands/status/__init__.py b/kicad_lib_manager/commands/status/__init__.py index 239e435..520d19c 100644 --- a/kicad_lib_manager/commands/status/__init__.py +++ b/kicad_lib_manager/commands/status/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import status -__all__ = ["status"] +# Create Typer app and add the status command as callback +status_app = typer.Typer( + name="status", + help="Show current library configuration status", + rich_markup_mode="rich", + callback=status, + invoke_without_command=True, +) + +__all__ = ["status", "status_app"] diff --git a/kicad_lib_manager/commands/status/command.py b/kicad_lib_manager/commands/status/command.py index d0b001f..61860d6 100644 --- a/kicad_lib_manager/commands/status/command.py +++ b/kicad_lib_manager/commands/status/command.py @@ -1,179 +1,208 @@ """ -Status command implementation for KiCad Library Manager. +Status command implementation for KiCad Library Manager (Typer version). """ import json from pathlib import Path +from typing import Annotated -import click +import typer import yaml +from rich.console import Console +from rich.panel import Panel +from rich.table import Table +from rich.text import Text -from ...library_manager import find_kicad_config, list_configured_libraries +from ...services.kicad_service import KiCadService +from ...utils.constants import CONFIG_DIR_NAME, CONFIG_FILE_NAME from ...utils.metadata import read_cloud_metadata, read_github_metadata +console = Console() -@click.command() -def status(): + +def status( + verbose: Annotated[ + bool, + typer.Option( + "--verbose", + "-v", + help="Show detailed configured libraries tables", + ), + ] = False, +) -> None: """Show the current KiCad configuration status""" try: + # Initialize services + kicad_service = KiCadService() + # Show KILM configuration first - try: - config_file = Path.home() / ".config" / "kicad-lib-manager" / "config.yaml" - if config_file.exists(): - click.echo("KILM Configuration:") - try: - with config_file.open() as f: - config_data = yaml.safe_load(f) - - # Show libraries - if ( - config_data - and "libraries" in config_data - and config_data["libraries"] - ): - click.echo(" Configured Libraries:") - - # Group by type - github_libs = [ - lib - for lib in config_data["libraries"] - if lib.get("type") == "github" - ] - cloud_libs = [ - lib - for lib in config_data["libraries"] - if lib.get("type") == "cloud" - ] - - if github_libs: - click.echo(" GitHub Libraries (symbols/footprints):") - for lib in github_libs: - name = lib.get("name", "unnamed") - path = lib.get("path", "unknown") - current = ( - " (current)" - if config_data - and config_data.get("current_library") == path - else "" - ) - click.echo(f" - {name}: {path}{current}") - - # Check if metadata file exists - try: - if read_github_metadata(Path(path)): - click.echo(" Metadata: Yes") - except Exception: - pass - - if cloud_libs: - click.echo(" Cloud Libraries (3D models):") - for lib in cloud_libs: - name = lib.get("name", "unnamed") - path = lib.get("path", "unknown") - current = ( - " (current)" - if config_data - and config_data.get("current_library") == path - else "" - ) - click.echo(f" - {name}: {path}{current}") - - # Check if metadata file exists - try: - if read_cloud_metadata(Path(path)): - click.echo(" Metadata: Yes") - except Exception: - pass - else: - click.echo(" No libraries configured") - - # Show current library - if ( - config_data - and "current_library" in config_data - and config_data["current_library"] - ): - click.echo( - f" Current Library: {config_data['current_library']}" - ) - else: - click.echo(" No current library set") - - # Show other settings - if config_data and "max_backups" in config_data: - click.echo(f" Max Backups: {config_data['max_backups']}") - - except Exception as e: - click.echo(f" Error reading configuration: {e}", err=True) - else: - click.echo( - "No KILM configuration file found. Run 'kilm init' to create one." - ) - except Exception as e: - click.echo(f"Error reading KILM configuration: {e}", err=True) + _show_kilm_configuration() - click.echo("\n--- KiCad Configuration ---") + console.print("\n[bold cyan]KiCad Configuration[/bold cyan]") - kicad_config = find_kicad_config() - click.echo(f"KiCad configuration directory: {kicad_config}") + kicad_config = kicad_service.find_kicad_config_dir() + console.print(f"KiCad configuration directory: [blue]{kicad_config}[/blue]") # Check environment variables in KiCad common - kicad_common = kicad_config / "kicad_common.json" - if kicad_common.exists(): + _show_kicad_environment_variables(kicad_config) + + # Check pinned libraries + _check_pinned_libraries(kicad_config, kicad_service) + + # Check configured libraries (only in verbose mode) + if verbose: + _show_configured_libraries(kicad_config, kicad_service) + + except Exception as e: + console.print(f"[red]Error getting KiCad configuration: {e}[/red]") + raise typer.Exit(1) from e + + +def _show_kilm_configuration() -> None: + """Show KILM configuration section""" + try: + config_file = Path.home() / ".config" / CONFIG_DIR_NAME / CONFIG_FILE_NAME + if config_file.exists(): + console.print("[bold cyan]KILM Configuration[/bold cyan]") try: - with kicad_common.open() as f: - common_config = json.load(f) - - click.echo("\nEnvironment Variables in KiCad:") - if ( - "environment" in common_config - and "vars" in common_config["environment"] - ): - env_vars = common_config["environment"]["vars"] - if env_vars: - for key, value in env_vars.items(): - click.echo(f" {key} = {value}") - else: - click.echo(" No environment variables set") + with config_file.open() as f: + config_data = yaml.safe_load(f) + + if config_data is not None: + _show_configured_libraries_table(config_data) + _show_kilm_settings(config_data) else: - click.echo(" No environment variables found") + console.print( + "[yellow]Configuration file is empty or invalid[/yellow]" + ) + except Exception as e: - click.echo(f" Error reading KiCad common configuration: {e}", err=True) + console.print(f"[red]Error reading configuration: {e}[/red]") + else: + console.print( + "[yellow]No KILM configuration file found. Run 'kilm init' to create one.[/yellow]" + ) + except Exception as e: + console.print(f"[red]Error reading KILM configuration: {e}[/red]") - # Check pinned libraries - check_pinned_libraries(kicad_config) - # Check configured libraries - try: - sym_libs, fp_libs = list_configured_libraries(kicad_config) - - click.echo("\nConfigured Symbol Libraries:") - if sym_libs: - for lib in sym_libs: - lib_name = lib["name"] - lib_uri = lib["uri"] - click.echo(f" - {lib_name}: {lib_uri}") - else: - click.echo(" No symbol libraries configured") - - click.echo("\nConfigured Footprint Libraries:") - if fp_libs: - for lib in fp_libs: - lib_name = lib["name"] - lib_uri = lib["uri"] - click.echo(f" - {lib_name}: {lib_uri}") - else: - click.echo(" No footprint libraries configured") - except Exception as e: - click.echo(f"Error listing configured libraries: {e}", err=True) +def _show_configured_libraries_table(config_data: dict) -> None: + """Show configured libraries in a table format""" + if not config_data or not config_data.get("libraries"): + console.print("[yellow] No libraries configured[/yellow]") + return + + # Create tables for different library types + github_libs = [ + lib for lib in config_data["libraries"] if lib.get("type") == "github" + ] + cloud_libs = [lib for lib in config_data["libraries"] if lib.get("type") == "cloud"] + + current_library = config_data.get("current_library", "") + + if github_libs: + table = Table(title="GitHub Libraries (symbols/footprints)", show_header=True) + table.add_column("Name", style="cyan", no_wrap=True) + table.add_column("Path", style="blue") + table.add_column("Status", justify="center") + table.add_column("Metadata", justify="center") + + for lib in github_libs: + name = lib.get("name", "unnamed") + path = lib.get("path", "unknown") + is_current = "✓ Current" if current_library == path else "" + + # Check metadata + try: + has_metadata = "✓" if read_github_metadata(Path(path)) else "✗" + except Exception: + has_metadata = "?" + + table.add_row(name, path, is_current, has_metadata) + + console.print(table) + + if cloud_libs: + table = Table(title="Cloud Libraries (3D models)", show_header=True) + table.add_column("Name", style="cyan", no_wrap=True) + table.add_column("Path", style="blue") + table.add_column("Status", justify="center") + table.add_column("Metadata", justify="center") + for lib in cloud_libs: + name = lib.get("name", "unnamed") + path = lib.get("path", "unknown") + is_current = "✓ Current" if current_library == path else "" + + # Check metadata + try: + has_metadata = "✓" if read_cloud_metadata(Path(path)) else "✗" + except Exception: + has_metadata = "?" + + table.add_row(name, path, is_current, has_metadata) + + console.print(table) + + +def _show_kilm_settings(config_data: dict) -> None: + """Show KILM settings""" + if not config_data: + return + + settings_text = Text() + + # Current library + current_lib = config_data.get("current_library") + if current_lib: + settings_text.append(f"Current Library: {current_lib}\n", style="green") + else: + settings_text.append("No current library set\n", style="yellow") + + # Max backups + max_backups = config_data.get("max_backups") + if max_backups is not None: + settings_text.append(f"Max Backups: {max_backups}", style="blue") + + if settings_text.plain: + console.print(Panel(settings_text, title="Settings", border_style="blue")) + + +def _show_kicad_environment_variables(kicad_config: Path) -> None: + """Show KiCad environment variables""" + kicad_common = kicad_config / "kicad_common.json" + if not kicad_common.exists(): + console.print("[yellow]No kicad_common.json found[/yellow]") + return + + try: + with kicad_common.open() as f: + common_config = json.load(f) + + console.print("\n[bold]Environment Variables in KiCad:[/bold]") + + if "environment" in common_config and "vars" in common_config["environment"]: + env_vars = common_config["environment"]["vars"] + if env_vars: + table = Table(show_header=True) + table.add_column("Variable", style="cyan", no_wrap=True) + table.add_column("Value", style="blue") + + for key, value in env_vars.items(): + table.add_row(key, str(value)) + + console.print(table) + else: + console.print("[yellow] No environment variables set[/yellow]") + else: + console.print("[yellow] No environment variables found[/yellow]") except Exception as e: - click.echo(f"Error getting KiCad configuration: {e}", err=True) + console.print(f"[red]Error reading KiCad common configuration: {e}[/red]") -def check_pinned_libraries(kicad_config): +def _check_pinned_libraries(kicad_config: Path, kicad_service: KiCadService) -> None: """Check and display pinned libraries""" - # First look in kicad_common.json + _ = kicad_service # Suppress unused warning, TODO: implement functionality kicad_common = kicad_config / "kicad_common.json" if kicad_common.exists(): try: @@ -181,7 +210,7 @@ def check_pinned_libraries(kicad_config): common_config = json.load(f) found_pinned = False - click.echo("\nPinned Libraries in KiCad:") + console.print("\n[bold]Pinned Libraries in KiCad:[/bold]") # Check for pinned symbol libraries if ( @@ -191,9 +220,9 @@ def check_pinned_libraries(kicad_config): sym_libs = common_config["session"]["pinned_symbol_libs"] if sym_libs: found_pinned = True - click.echo(" Symbol Libraries:") + console.print("[cyan]Symbol Libraries:[/cyan]") for lib in sym_libs: - click.echo(f" - {lib}") + console.print(f" • {lib}") # Check for pinned footprint libraries if ( @@ -203,18 +232,19 @@ def check_pinned_libraries(kicad_config): fp_libs = common_config["session"]["pinned_fp_libs"] if fp_libs: found_pinned = True - click.echo(" Footprint Libraries:") + console.print("[cyan]Footprint Libraries:[/cyan]") for lib in fp_libs: - click.echo(f" - {lib}") + console.print(f" • {lib}") if not found_pinned: - click.echo(" No pinned libraries found in kicad_common.json") + console.print( + "[yellow] No pinned libraries found in kicad_common.json[/yellow]" + ) return except Exception as e: - click.echo( - f" Error reading pinned libraries from kicad_common.json: {e}", - err=True, + console.print( + f"[red]Error reading pinned libraries from kicad_common.json: {e}[/red]" ) # Fall back to the old method of looking for a separate pinned file @@ -224,28 +254,62 @@ def check_pinned_libraries(kicad_config): with pinned_libs.open() as f: pinned_config = json.load(f) - click.echo("\nPinned Libraries in KiCad (legacy format):") + console.print("\n[bold]Pinned Libraries in KiCad (legacy format):[/bold]") found_pinned = False if "pinned_symbol_libs" in pinned_config: sym_libs = pinned_config["pinned_symbol_libs"] if sym_libs: found_pinned = True - click.echo(" Symbol Libraries:") + console.print("[cyan]Symbol Libraries:[/cyan]") for lib in sym_libs: - click.echo(f" - {lib}") + console.print(f" • {lib}") if "pinned_footprint_libs" in pinned_config: fp_libs = pinned_config["pinned_footprint_libs"] if fp_libs: found_pinned = True - click.echo(" Footprint Libraries:") + console.print("[cyan]Footprint Libraries:[/cyan]") for lib in fp_libs: - click.echo(f" - {lib}") + console.print(f" • {lib}") if not found_pinned: - click.echo(" No pinned libraries found") + console.print("[yellow] No pinned libraries found[/yellow]") except Exception as e: - click.echo(f" Error reading pinned libraries: {e}", err=True) + console.print(f"[red]Error reading pinned libraries: {e}[/red]") else: - click.echo("\nNo pinned libraries file found") + console.print("\n[yellow]No pinned libraries file found[/yellow]") + + +def _show_configured_libraries(kicad_config: Path, kicad_service: KiCadService) -> None: + """Show configured libraries in KiCad""" + try: + sym_libs, fp_libs = kicad_service.get_configured_libraries(kicad_config) + + console.print("\n[bold]Configured Symbol Libraries:[/bold]") + if sym_libs: + table = Table(show_header=True) + table.add_column("Library Name", style="cyan", no_wrap=True) + table.add_column("URI", style="blue") + + for lib in sym_libs: + table.add_row(lib.get("name", ""), lib.get("uri", "")) + + console.print(table) + else: + console.print("[yellow] No symbol libraries configured[/yellow]") + + console.print("\n[bold]Configured Footprint Libraries:[/bold]") + if fp_libs: + table = Table(show_header=True) + table.add_column("Library Name", style="cyan", no_wrap=True) + table.add_column("URI", style="blue") + + for lib in fp_libs: + table.add_row(lib.get("name", ""), lib.get("uri", "")) + + console.print(table) + else: + console.print("[yellow] No footprint libraries configured[/yellow]") + except Exception as e: + console.print(f"[red]Error listing configured libraries: {e}[/red]") diff --git a/kicad_lib_manager/commands/status/docs.mdx b/kicad_lib_manager/commands/status/docs.mdx index 04c11c8..20ec178 100644 --- a/kicad_lib_manager/commands/status/docs.mdx +++ b/kicad_lib_manager/commands/status/docs.mdx @@ -15,6 +15,9 @@ kilm status [OPTIONS] ## Options +- `-v, --verbose`: + Show detailed configured library tables (symbol and footprint tables from KiCad). Without this flag, only the high-level sections are shown. + - `--help`: Show the help message and exit. @@ -26,7 +29,7 @@ The command gathers and displays information about: - **KiCad Configuration Directory:** The location KiLM detected for KiCad's configuration files. - **KiCad Environment Variables:** Lists the environment variables currently set within KiCad's `kicad_common.json` file. - **KiCad Pinned Libraries:** Shows the symbol and footprint libraries currently marked as favorites (pinned) in `kicad_common.json`. -- **KiCad Configured Libraries:** Lists the symbol and footprint libraries currently present in KiCad's `sym-lib-table` and `fp-lib-table`. +- **KiCad Configured Libraries:** Lists the symbol and footprint libraries currently present in KiCad's `sym-lib-table` and `fp-lib-table`. Requires `-v/--verbose`. ## Example diff --git a/kicad_lib_manager/commands/sync/__init__.py b/kicad_lib_manager/commands/sync/__init__.py index 4f15a3d..b6f578f 100644 --- a/kicad_lib_manager/commands/sync/__init__.py +++ b/kicad_lib_manager/commands/sync/__init__.py @@ -1,7 +1,14 @@ -""" -Sync command - Update/sync library content from git repositories -""" +import typer from .command import sync -__all__ = ["sync", "check_for_library_changes"] +# Create Typer app and add the sync command as callback +sync_app = typer.Typer( + name="sync", + help="Update/sync library content from git repositories", + rich_markup_mode="rich", + callback=sync, + invoke_without_command=True, +) + +__all__ = ["sync", "sync_app"] diff --git a/kicad_lib_manager/commands/sync/command.py b/kicad_lib_manager/commands/sync/command.py index b5ad7d9..878334a 100644 --- a/kicad_lib_manager/commands/sync/command.py +++ b/kicad_lib_manager/commands/sync/command.py @@ -5,35 +5,28 @@ import subprocess from pathlib import Path +from typing import Annotated -import click - -from ...config import Config - - -@click.command() -@click.option( - "--dry-run", - is_flag=True, - default=False, - help="Show what would be synced without making changes", - show_default=True, -) -@click.option( - "--verbose", - is_flag=True, - default=False, - help="Show detailed output", - show_default=True, -) -@click.option( - "--auto-setup", - is_flag=True, - default=False, - help="Run 'kilm setup' automatically if new libraries are detected", - show_default=True, -) -def sync(dry_run, verbose, auto_setup): +import typer +from rich.console import Console + +from ...services.config_service import Config + +console = Console() + + +def sync( + dry_run: Annotated[ + bool, typer.Option(help="Show what would be synced without making changes") + ] = False, + verbose: Annotated[bool, typer.Option(help="Show detailed output")] = False, + auto_setup: Annotated[ + bool, + typer.Option( + help="Run 'kilm setup' automatically if new libraries are detected" + ), + ] = False, +) -> None: """Sync all configured GitHub libraries with git pull. This command syncs all configured GitHub libraries (symbols/footprints) @@ -44,150 +37,167 @@ def sync(dry_run, verbose, auto_setup): and recommend running 'kilm setup' if needed. Use --auto-setup to run setup automatically when new libraries are detected. """ - config = Config() - - # Get GitHub libraries from config (symbols/footprints) - libraries = config.get_libraries(library_type="github") - - if not libraries: - click.echo("No GitHub libraries configured. Use 'kilm init' to add a library.") - return - - click.echo(f"Syncing {len(libraries)} KiCad GitHub libraries...") - - updated_count = 0 # Actually changed - up_to_date_count = 0 # Already at latest version - skipped_count = 0 # Could not sync (not git, etc.) - failed_count = 0 # Git pull failed - - # Track libraries that have changes that might require setup - libraries_with_changes = [] - - for lib in libraries: - lib_name = lib.get("name", "unnamed") - lib_path = lib.get("path") - - if not lib_path: - click.echo(f" Skipping {lib_name}: No path defined") - skipped_count += 1 - continue - - lib_path = Path(lib_path) - if not lib_path.exists(): - click.echo(f" Skipping {lib_name}: Path does not exist: {lib_path}") - skipped_count += 1 - continue - - git_dir = lib_path / ".git" - if not git_dir.exists() or not git_dir.is_dir(): - click.echo(f" Skipping {lib_name}: Not a git repository: {lib_path}") - skipped_count += 1 - continue - - # Prepare to run git pull - click.echo(f" Syncing {lib_name} at {lib_path}...") - - if dry_run: - click.echo(f" Dry run: would execute 'git pull' in {lib_path}") - updated_count += 1 - continue - - try: - # Run git pull - result = subprocess.run( - ["git", "pull"], - cwd=lib_path, - capture_output=True, - text=True, - check=False, + try: + config = Config() + + # Get GitHub libraries from config (symbols/footprints) + libraries = config.get_libraries(library_type="github") + + if not libraries: + console.print( + "[yellow]No GitHub libraries configured. Use 'kilm init' to add a library.[/yellow]" ) + return - if result.returncode == 0: - output = result.stdout.strip() or "Already up to date." - is_updated = "Already up to date" not in output + console.print( + f"[cyan]Syncing {len(libraries)} KiCad GitHub libraries...[/cyan]" + ) - if verbose: - click.echo(f" Success: {output}") - # Also show the short status for consistency - if is_updated: - click.echo(" Updated") + updated_count = 0 # Actually changed + up_to_date_count = 0 # Already at latest version + skipped_count = 0 # Could not sync (not git, etc.) + failed_count = 0 # Git pull failed + + # Track libraries that have changes that might require setup + libraries_with_changes: list[tuple[str, Path, list[str]]] = [] + + for lib in libraries: + lib_name = lib.get("name", "unnamed") + lib_path = lib.get("path") + + if not lib_path: + console.print( + f" [yellow]Skipping {lib_name}: No path defined[/yellow]" + ) + skipped_count += 1 + continue + + lib_path = Path(lib_path) + if not lib_path.exists(): + console.print( + f" [yellow]Skipping {lib_name}: Path does not exist: {lib_path}[/yellow]" + ) + skipped_count += 1 + continue + + git_dir = lib_path / ".git" + if not git_dir.exists() or not git_dir.is_dir(): + console.print( + f" [yellow]Skipping {lib_name}: Not a git repository: {lib_path}[/yellow]" + ) + skipped_count += 1 + continue + + # Prepare to run git pull + console.print(f" [cyan]Syncing {lib_name} at {lib_path}...[/cyan]") + + if dry_run: + console.print( + f" [blue]Dry run: would execute 'git pull' in {lib_path}[/blue]" + ) + updated_count += 1 + continue + + try: + # Run git pull + result = subprocess.run( + ["git", "pull"], + cwd=lib_path, + capture_output=True, + text=True, + check=False, + ) + + if result.returncode == 0: + output = result.stdout.strip() or "Already up to date." + is_updated = "Already up to date" not in output + + if verbose: + console.print(f" [green]Success: {output}[/green]") + # Also show the short status for consistency + if is_updated: + console.print(" [green]Updated[/green]") + else: + console.print(" [blue]Up to date[/blue]") else: - click.echo(" Up to date") - else: + if is_updated: + console.print(" [green]Updated[/green]") + else: + console.print(" [blue]Up to date[/blue]") + + # Update counters regardless of verbose flag if is_updated: - click.echo(" Updated") + updated_count += 1 else: - click.echo(" Up to date") + up_to_date_count += 1 - # Update counters regardless of verbose flag - if is_updated: - updated_count += 1 - else: - up_to_date_count += 1 - - # Check if there are new library files that would require setup - if is_updated: - changes_require_setup = check_for_library_changes(lib_path) - if changes_require_setup: - libraries_with_changes.append( - (lib_name, lib_path, changes_require_setup) - ) - if verbose: - click.echo( - f" Detected new library content: {', '.join(changes_require_setup)}" + # Check if there are new library files that would require setup + if is_updated: + changes_require_setup = check_for_library_changes(lib_path) + if changes_require_setup: + libraries_with_changes.append( + (lib_name, lib_path, changes_require_setup) ) - else: - click.echo(f" Failed: {result.stderr.strip()}") - failed_count += 1 + if verbose: + console.print( + f" [yellow]Detected new library content: {', '.join(changes_require_setup)}[/yellow]" + ) + else: + console.print(f" [red]Failed: {result.stderr.strip()}[/red]") + failed_count += 1 - except Exception as e: - click.echo(f" Error: {str(e)}") - failed_count += 1 - - # Summary - click.echo("\nSync Summary:") - click.echo(f" {updated_count} libraries synced") - click.echo(f" {up_to_date_count} libraries up to date") - click.echo(f" {skipped_count} libraries skipped") - click.echo(f" {failed_count} libraries failed") - - # If libraries with changes were detected, suggest running setup - if libraries_with_changes: - click.echo("\nNew library content detected in:") - for lib_name, _lib_path, changes in libraries_with_changes: - click.echo(f" - {lib_name}: {', '.join(changes)}") - - if auto_setup: - click.echo("\nRunning 'kilm setup' to configure new libraries...") - # Import at runtime to avoid circular imports - try: - from ...commands.setup import setup as setup_cmd + except Exception as e: + console.print(f" [red]Error: {str(e)}[/red]") + failed_count += 1 - # Create context using the command's built-in context factory - ctx = setup_cmd.make_context( - "setup", args=[], parent=click.get_current_context() + # Summary + console.print("\n[bold cyan]Sync Summary:[/bold cyan]") + console.print(f" [green]{updated_count}[/green] libraries synced") + console.print(f" [blue]{up_to_date_count}[/blue] libraries up to date") + console.print(f" [yellow]{skipped_count}[/yellow] libraries skipped") + console.print(f" [red]{failed_count}[/red] libraries failed") + + # If libraries with changes were detected, suggest running setup + if libraries_with_changes: + console.print("\n[yellow]New library content detected in:[/yellow]") + for lib_name, _lib_path, changes in libraries_with_changes: + console.print(f" - [cyan]{lib_name}[/cyan]: {', '.join(changes)}") + + if auto_setup: + console.print( + "\n[green]Running 'kilm setup' to configure new libraries...[/green]" + ) + # Import at runtime to avoid circular imports + try: + from ...commands.setup.command import setup as setup_cmd + + setup_cmd() + except ImportError: + console.print( + "[red]Error: Could not import setup command. Please run 'kilm setup' manually.[/red]" + ) + else: + console.print( + "\n[yellow]You should run 'kilm setup' to configure the new libraries in KiCad.[/yellow]" ) - setup_cmd.invoke(ctx) - except ImportError: - click.echo( - "Error: Could not import setup command. Please run 'kilm setup' manually." + console.print( + "[blue]Run 'kilm sync --auto-setup' next time to automatically run setup after sync.[/blue]" ) else: - click.echo( - "\nYou should run 'kilm setup' to configure the new libraries in KiCad." + console.print( + "\n[blue]No new libraries detected that would require running 'kilm setup'.[/blue]" ) - click.echo( - "Run 'kilm sync --auto-setup' next time to automatically run setup after sync." + console.print( + "[blue]Use 'kilm status' to check your current configuration.[/blue]" ) - else: - click.echo( - "\nNo new libraries detected that would require running 'kilm setup'." - ) - click.echo("Use 'kilm status' to check your current configuration.") + + except Exception as e: + console.print(f"[red]Error: {e}[/red]") + raise typer.Exit(1) from e # TODO: Should be in services or utils -def check_for_library_changes(lib_path): +def check_for_library_changes(lib_path: Path) -> list[str]: """ Check if git pull changes indicate new libraries that would require setup. Uses git diff to analyze what files were added/changed in the pull. @@ -252,13 +262,13 @@ def check_for_library_changes(lib_path): return changes -def _is_symbol_library_change(path): +def _is_symbol_library_change(path: str) -> bool: """Check if a path change indicates a symbol library change.""" # Look for .kicad_sym files in symbols directory return path.startswith("symbols/") and path.endswith(".kicad_sym") -def _is_footprint_library_change(path): +def _is_footprint_library_change(path: str) -> bool: """Check if a path change indicates a footprint library change.""" # Look for .pretty directories or files within them return (path.startswith("footprints/") and path.endswith(".pretty")) or ( @@ -266,13 +276,13 @@ def _is_footprint_library_change(path): ) -def _is_template_change(path): +def _is_template_change(path: str) -> bool: """Check if a path change indicates a template change.""" # Look for metadata.yaml files in template directories return path.startswith("templates/") and path.endswith("metadata.yaml") -def _check_current_library_state(lib_path): +def _check_current_library_state(lib_path: Path) -> list[str]: """ Fallback method to check current library state when git diff is not available. This is used when we can't determine what changed in the pull. diff --git a/kicad_lib_manager/commands/template/__init__.py b/kicad_lib_manager/commands/template/__init__.py index 4d98761..0c1fd19 100644 --- a/kicad_lib_manager/commands/template/__init__.py +++ b/kicad_lib_manager/commands/template/__init__.py @@ -1,3 +1,22 @@ -from .command import template +import typer -__all__ = ["template"] +from .command import create, list_templates, main_callback, make + +# Create Typer app with subcommands +template_app = typer.Typer( + name="template", + help="Manage KiCad project templates", + rich_markup_mode="rich", + callback=main_callback, + invoke_without_command=True, + no_args_is_help=True, +) + +# Add subcommands +template_app.command("create", help="Create a new KiCad project from a template")( + create +) +template_app.command("make", help="Create a template from an existing project")(make) +template_app.command("list", help="List all available templates")(list_templates) + +__all__ = ["template_app", "create", "make", "list_templates"] diff --git a/kicad_lib_manager/commands/template/command.py b/kicad_lib_manager/commands/template/command.py index d6bf8bb..45015fe 100644 --- a/kicad_lib_manager/commands/template/command.py +++ b/kicad_lib_manager/commands/template/command.py @@ -9,14 +9,18 @@ import sys import traceback from pathlib import Path +from typing import Annotated, Optional -import click import jinja2 import pathspec import questionary +import typer import yaml +from rich.console import Console +from rich.panel import Panel +from rich.table import Table -from ...config import Config +from ...services.config_service import Config from ...utils.template import ( HOOKS_DIR, POST_CREATE_HOOK, @@ -32,9 +36,11 @@ render_template_string, ) +console = Console() +err_console = Console(stderr=True) -@click.group() -def template(): + +def main_callback() -> None: """Manage KiCad project templates. This command group allows you to create new KiCad projects from templates, @@ -43,39 +49,33 @@ def template(): pass -@template.command() -@click.argument("name", required=False) -@click.argument("directory", required=False, type=click.Path()) -@click.option( - "--template", - help="Name of the template to use", - default=None, -) -@click.option( - "--library", - help="Name of the library containing the template", - default=None, -) -@click.option( - "--set-var", - multiple=True, - help="Set template variable in key=value format", -) -@click.option( - "--dry-run", - is_flag=True, - default=False, - help="Show what would be created without making changes", - show_default=True, -) -@click.option( - "--skip-hooks", - is_flag=True, - default=False, - help="Skip post-creation hooks", - show_default=True, -) -def create(name, directory, template, library, set_var, dry_run, skip_hooks): +def create( + name: Annotated[ + Optional[str], typer.Argument(help="Name of the project to create") + ] = None, + directory: Annotated[ + Optional[str], typer.Argument(help="Directory to create project in") + ] = None, + template: Annotated[ + Optional[str], typer.Option(help="Name of the template to use") + ] = None, + library: Annotated[ + Optional[str], typer.Option(help="Name of the library containing the template") + ] = None, + set_var: Annotated[ + Optional[list[str]], + typer.Option("--set-var", help="Set template variable in key=value format"), + ] = None, + dry_run: Annotated[ + bool, + typer.Option( + "--dry-run", help="Show what would be created without making changes" + ), + ] = False, + skip_hooks: Annotated[ + bool, typer.Option("--skip-hooks", help="Skip post-creation hooks") + ] = False, +) -> None: """Create a new KiCad project from a template. Creates a new KiCad project from a template in one of the configured libraries. @@ -114,8 +114,8 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): all_templates = find_all_templates(config) if not all_templates: - click.echo("No templates found in any configured libraries.") - click.echo("Use 'kilm template make' to create a template first.") + console.print("No templates found in any configured libraries.") + console.print("Use 'kilm template make' to create a template first.") return # Parse name and directory @@ -153,12 +153,12 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): break if not selected_template: - click.echo(f"Template '{template}' not found.") - click.echo("Available templates:") + console.print(f"Template '{template}' not found.") + console.print("Available templates:") for t_name, t_data in all_templates.items(): library = t_data.get("source_library", "unknown") description = t_data.get("description", "") - click.echo(f" {t_name} ({library}): {description}") + console.print(f" {t_name} ({library}): {description}") return else: # Interactive template selection using questionary @@ -175,22 +175,21 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): ).ask() if selected_template_name is None: # Handle cancellation - click.echo("Template selection cancelled.") + console.print("Template selection cancelled.") return selected_template = all_templates.get(selected_template_name) # Ensure selected_template is not None before proceeding if not selected_template: - click.echo("Error: No template selected.", err=True) + console.print("[red]Error: No template selected.[/red]") return # Get template directory template_path_str = selected_template.get("path") if not template_path_str: - click.echo( - f"Error: Template metadata for '{selected_template.get('name')}' is missing the 'path'.", - err=True, + err_console.print( + f"Error: Template metadata for '{selected_template.get('name')}' is missing the 'path'." ) return template_dir = Path(template_path_str) @@ -201,12 +200,12 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): with metadata_file.open() as f: metadata = yaml.safe_load(f) if not metadata: # Handle empty metadata file - click.echo( - f"Warning: Template metadata file is empty: {metadata_file}", err=True + err_console.print( + f"Warning: Template metadata file is empty: {metadata_file}" ) metadata = {} # Use empty dict to avoid downstream errors except Exception as e: - click.echo(f"Error reading template metadata {metadata_file}: {e}", err=True) + err_console.print(f"Error reading template metadata {metadata_file}: {e}") return # Get template variables from metadata @@ -217,7 +216,7 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): command_line_vars = {} # Variables set via --set-var # Parse --set-var options first - for var in set_var: + for var in set_var or []: if "=" in var: key, value = var.split("=", 1) command_line_vars[key.strip()] = value.strip() @@ -238,14 +237,14 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): variables["project_name"] = name # Show template info - click.echo() - click.echo(f"Using Template: {metadata.get('name', 'Unknown')}") - click.echo(f"Description: {metadata.get('description', 'N/A')}") + console.print() + console.print(f"Using Template: {metadata.get('name', 'Unknown')}") + console.print(f"Description: {metadata.get('description', 'N/A')}") if metadata.get("use_case"): - click.echo(f"Use case: {metadata.get('use_case')}") + console.print(f"Use case: {metadata.get('use_case')}") - click.echo() - click.echo("Template Variables:") + console.print() + console.print("Template Variables:") # Combine initial variables from args and --set-var variables.update(command_line_vars) @@ -263,7 +262,7 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): else "--set-var" ) if var_name in command_line_vars or (var_name == "project_name" and name): - click.echo( + console.print( f" {var_name}: {variables[var_name]} (from {source}) - {description}" ) continue # Skip prompting @@ -281,16 +280,15 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): except jinja2.exceptions.UndefinedError as e: # If a variable needed for the default hasn't been entered yet, # keep the original template string or empty if undefined errors occur early - click.echo( - f"Debug: Undefined variable for default of {var_name}: {e}. Default might be incomplete.", - err=True, + err_console.print( + f"Debug: Undefined variable for default of {var_name}: {e}. Default might be incomplete." ) rendered_default = ( "" # Or keep 'default'? Better to show empty than half-rendered? ) except Exception as e: - click.echo( - f"Warning: Could not render default for {var_name}: {e}", err=True + err_console.print( + f"Warning: Could not render default for {var_name}: {e}" ) rendered_default = default # Use original default on other errors @@ -302,7 +300,7 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): ).ask() if answer is None: # Handle Ctrl+C or cancellation - click.echo("Variable input cancelled.") + console.print("Variable input cancelled.") return # Exit the command gracefully # Store the answer for use in subsequent default renderings @@ -340,9 +338,8 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): original_default_template, variables ) except Exception as e: - click.echo( - f"Warning: Could not re-render default for {var_name}: {e}", - err=True, + err_console.print( + f"Warning: Could not re-render default for {var_name}: {e}" ) correct_default = variables[var_name] # Keep existing value on error @@ -352,14 +349,14 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): variables[var_name] == shown_default and variables[var_name] != correct_default ): - click.echo( + console.print( f"Updating default for '{var_name}': '{shown_default}' -> '{correct_default}'" ) variables[var_name] = correct_default # --- End Post-processing --- - click.echo() - click.echo( + console.print() + console.print( f"Final variables: {json.dumps(variables, indent=2)}" ) # Changed message for clarity @@ -381,9 +378,8 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): ) final_project_dir = project_dir / dir_name_rendered except Exception as e: - click.echo( - f"Warning: Could not render default directory_name '{directory_name_template}': {e}", - err=True, + err_console.print( + f"Warning: Could not render default directory_name '{directory_name_template}': {e}" ) # Fallback below @@ -391,47 +387,45 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): if not final_project_dir and "project_name" in variables: project_name_sanitized = variables["project_name"].lower().replace(" ", "-") final_project_dir = project_dir / project_name_sanitized - click.echo(f"Using project_name to determine directory: {final_project_dir}") + console.print(f"Using project_name to determine directory: {final_project_dir}") # Final check if we have a directory if not final_project_dir: - click.echo( - "Error: Cannot determine project directory. Ensure 'project_name' or 'directory_name' variable is properly handled.", - err=True, + err_console.print( + "Error: Cannot determine project directory. Ensure 'project_name' or 'directory_name' variable is properly handled." ) return # --- End Directory Determination --- - click.echo() - click.echo(f"Project will be created in: {final_project_dir}") + console.print() + console.print(f"Project will be created in: {final_project_dir}") # --- Execution --- if dry_run: - click.echo("Dry run enabled. No changes will be made.") + console.print("Dry run enabled. No changes will be made.") # Optionally, list files that *would* be created here else: # Check if target directory exists and handle overwrite if final_project_dir.exists(): - if not click.confirm( + if not typer.confirm( f"Directory '{final_project_dir}' already exists. Overwrite?" ): - click.echo("Aborted.") + console.print("Aborted.") return else: - click.echo(f"Removing existing directory: {final_project_dir}") + console.print(f"Removing existing directory: {final_project_dir}") try: shutil.rmtree(final_project_dir) except Exception as e: - click.echo(f"Error removing existing directory: {e}", err=True) + err_console.print(f"Error removing existing directory: {e}") return # Create parent directories if they don't exist try: final_project_dir.parent.mkdir(parents=True, exist_ok=True) except Exception as e: - click.echo( - f"Error creating parent directories for {final_project_dir}: {e}", - err=True, + err_console.print( + f"Error creating parent directories for {final_project_dir}: {e}" ) return @@ -448,31 +442,25 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): old_syntax_files.append(file) if old_syntax_files: - click.echo() - click.echo("WARNING: Windows Compatibility Notice:", err=True) - click.echo( - "This template uses the old {{variable}} syntax in filenames, which may not work on Windows.", - err=True, + console.print() + err_console.print("WARNING: Windows Compatibility Notice:") + err_console.print( + "This template uses the old {{variable}} syntax in filenames, which may not work on Windows." ) - click.echo( - "Consider updating the template to use the new Windows-compatible %{variable} syntax.", - err=True, + err_console.print( + "Consider updating the template to use the new Windows-compatible %{variable} syntax." ) - click.echo("Files with old syntax:", err=True) + err_console.print("Files with old syntax:") for file in old_syntax_files[:3]: # Show first 3 files - click.echo(f" - {file}", err=True) + err_console.print(f" - {file}") if len(old_syntax_files) > 3: - click.echo( - f" ... and {len(old_syntax_files) - 3} more", err=True - ) - click.echo() - click.echo("New syntax examples:", err=True) - click.echo(" - %{project_name}.kicad_pro", err=True) - click.echo(" - %{project_name.lower}.kicad_sch", err=True) - click.echo( - " - %{project_name.replace(' ', '-')}.kicad_pcb", err=True - ) - click.echo() + err_console.print(f" ... and {len(old_syntax_files) - 3} more") + console.print() + err_console.print("New syntax examples:") + err_console.print(" - %{project_name}.kicad_pro") + err_console.print(" - %{project_name.lower}.kicad_sch") + err_console.print(" - %{project_name.replace(' ', '-')}.kicad_pcb") + console.print() success = create_project_from_template( template_dir=template_dir, @@ -483,93 +471,68 @@ def create(name, directory, template, library, set_var, dry_run, skip_hooks): metadata=metadata, # Pass metadata for post-hook if needed ) except Exception as e: - click.echo(f"Error during project creation: {e}", err=True) + err_console.print(f"Error during project creation: {e}") traceback.print_exc() # Print traceback for debugging success = False if success: if not dry_run: - click.echo() - click.echo( + console.print() + console.print( f"Project '{variables.get('project_name', name)}' created successfully in '{final_project_dir}'" ) else: - click.echo() - click.echo("Project creation failed.", err=True) + console.print() + err_console.print("Project creation failed.") -@template.command() -@click.argument("name", required=False) -@click.argument( - "source_directory", - required=False, - type=click.Path(exists=True, file_okay=False, dir_okay=True), -) -@click.option( - "--description", - help="Template description", - default=None, -) -@click.option( - "--use-case", - help="Template use case description", - default=None, -) -@click.option( - "--output-directory", - help="Directory where the template will be created", - type=click.Path(), - default=None, -) -@click.option( - "--exclude", - multiple=True, - help="Additional patterns to exclude (gitignore format)", -) -@click.option( - "--variable", - multiple=True, - help="Define a template variable in name=value format", -) -@click.option( - "--extends", - help="Parent template that this template extends", - default=None, -) -@click.option( - "--non-interactive", - is_flag=True, - default=False, - help="Non-interactive mode (don't prompt for variables or configuration)", - show_default=True, -) -@click.option( - "--dry-run", - is_flag=True, - default=False, - help="Show what would be created without making changes", - show_default=True, -) -@click.option( - "--force", - is_flag=True, - default=False, - help="Overwrite existing template if it exists", - show_default=True, -) def make( - name, - source_directory, - description, - use_case, - output_directory, - exclude, - variable, - extends, - non_interactive, - dry_run, - force, -): + name: Annotated[ + Optional[str], typer.Argument(help="Name of the template to create") + ] = None, + source_directory: Annotated[ + Optional[Path], typer.Argument(help="Source project directory") + ] = None, + description: Annotated[ + Optional[str], typer.Option(help="Template description") + ] = None, + use_case: Annotated[ + Optional[str], typer.Option("--use-case", help="Template use case description") + ] = None, + output_directory: Annotated[ + Optional[Path], + typer.Option( + "--output-directory", help="Directory where the template will be created" + ), + ] = None, + exclude: Annotated[ + Optional[list[str]], + typer.Option(help="Additional patterns to exclude (gitignore format)"), + ] = None, + variable: Annotated[ + Optional[list[str]], + typer.Option(help="Define a template variable in name=value format"), + ] = None, + extends: Annotated[ + Optional[str], typer.Option(help="Parent template that this template extends") + ] = None, + non_interactive: Annotated[ + bool, + typer.Option( + "--non-interactive", + help="Non-interactive mode (don't prompt for variables or configuration)", + ), + ] = False, + dry_run: Annotated[ + bool, + typer.Option( + "--dry-run", help="Show what would be created without making changes" + ), + ] = False, + force: Annotated[ + bool, typer.Option("--force", help="Overwrite existing template if it exists") + ] = False, +) -> None: """Create a template from an existing project. Creates a new KiCad project template from an existing project. If NAME @@ -616,7 +579,7 @@ def make( library_paths = {lib["name"]: lib["path"] for lib in all_libraries} if not library_names: - click.echo( + console.print( "No GitHub libraries configured. Use 'kilm init' to create one first." ) sys.exit(1) @@ -629,39 +592,39 @@ def make( # Ask for source directory if not specified if not source_directory: default_dir = str(Path.cwd()) - source_dir_input = click.prompt( + source_dir_input = typer.prompt( "Source project directory", default=default_dir ) # Handle relative paths if not Path(source_dir_input).is_absolute(): - source_directory = str(Path.cwd() / source_dir_input) + source_directory = Path.cwd() / source_dir_input else: - source_directory = source_dir_input + source_directory = Path(source_dir_input) # Ask for template name if not specified if not name: - default_name = Path(source_directory).name - name = click.prompt("Template name", default=default_name) + default_name = source_directory.name if source_directory else "template" + name = typer.prompt("Template name", default=default_name) # Ask for description and use case if not specified if not description: - description = click.prompt( + description = typer.prompt( "Template description", default=f"{name} template" ) if not use_case: - use_case = click.prompt("Template use case", default="") + use_case = typer.prompt("Template use case", default="") # Ask for output directory if not specified if not output_directory: # Show numbered list of libraries - click.echo("\nChoose a library to store the template:") + console.print("\nChoose a library to store the template:") for i, lib_name in enumerate(library_names): - click.echo(f"{i + 1}. {lib_name} ({library_paths[lib_name]})") + console.print(f"{i + 1}. {lib_name} ({library_paths[lib_name]})") # Get selection while True: try: - lib_selection = click.prompt( + lib_selection = typer.prompt( "Select library (number)", type=int, default=1 ) if 1 <= lib_selection <= len(library_names): @@ -669,65 +632,68 @@ def make( library_path = library_paths[selected_lib] break else: - click.echo( + console.print( f"Please enter a number between 1 and {len(library_names)}" ) except ValueError: - click.echo("Please enter a valid number") + console.print("Please enter a valid number") output_directory = Path(library_path) / TEMPLATES_DIR / name - click.echo(f"Template will be created in: {output_directory}") + console.print(f"Template will be created in: {output_directory}") else: # Non-interactive mode - use current directory if not specified if not source_directory: - source_directory = str(Path.cwd()) + source_directory = Path.cwd() # If name is not provided, use the directory name if not name: - name = Path(source_directory).name + name = source_directory.name # Determine the output directory if not output_directory: # Find the library to add the template to library_path = config.get_symbol_library_path() if not library_path: - click.echo( + console.print( "No library configured. Use 'kilm init' to create one first." ) return output_directory = Path(library_path) / TEMPLATES_DIR / name - # Convert source_directory to Path object - source_directory = Path(source_directory) + # Ensure source_directory is a Path object + if source_directory is None: + source_directory = Path.cwd() + elif not isinstance(source_directory, Path): + source_directory = Path(source_directory) # Check if template already exists - if output_directory and Path(output_directory).exists() and not force: - click.echo(f"Template '{name}' already exists at {output_directory}") - click.echo("Use --force to overwrite.") + if output_directory and output_directory.exists() and not force: + console.print(f"Template '{name}' already exists at {output_directory}") + console.print("Use --force to overwrite.") return # Get gitignore spec gitignore_spec = get_gitignore_spec(source_directory) # Show what we're going to do - click.echo(f"Creating template '{name}' from {source_directory}") - click.echo(f"Output directory: {output_directory}") + console.print(f"Creating template '{name}' from {source_directory}") + console.print(f"Output directory: {output_directory}") if description: - click.echo(f"Description: {description}") + console.print(f"Description: {description}") if use_case: - click.echo(f"Use case: {use_case}") + console.print(f"Use case: {use_case}") if exclude: - click.echo("Additional exclusions:") + console.print("Additional exclusions:") for pattern in exclude: - click.echo(f" {pattern}") + console.print(f" {pattern}") if extends: - click.echo(f"Extends: {extends}") + console.print(f"Extends: {extends}") # Parse variables from command line variable_dict = {} - for var in variable: + for var in variable or []: if "=" in var: key, value = var.split("=", 1) variable_dict[key.strip()] = { @@ -736,27 +702,27 @@ def make( } if variable_dict: - click.echo("\nTemplate variables:") + console.print("\nTemplate variables:") for key, value in variable_dict.items(): - click.echo(f" {key}: {value['default']} - {value['description']}") + console.print(f" {key}: {value['default']} - {value['description']}") # If interactive mode is enabled, scan for potential variables detected_variables = {} if interactive: potential_vars = find_potential_variables(source_directory) if potential_vars: - click.echo("\nFound potential template variables:") + console.print("\nFound potential template variables:") for var_name, values in potential_vars.items(): value_str = ", ".join(values) - click.echo(f" {var_name}: {value_str}") + console.print(f" {var_name}: {value_str}") # Ask if the user wants to use this variable - if click.confirm( + if typer.confirm( f" Use '{var_name}' as a template variable?", default=True ): # Use the first value as default default_value = values[0] if values else "" - description = click.prompt( + description = typer.prompt( " Description", default=f"Value for {var_name}" ) @@ -766,12 +732,12 @@ def make( } # Always ask if the user wants to define additional variables - while click.confirm( + while typer.confirm( "Would you like to define additional template variables?", default=False ): - var_name = click.prompt("Variable name") - var_default = click.prompt("Default value", default="") - var_description = click.prompt( + var_name = typer.prompt("Variable name") + var_default = typer.prompt("Default value", default="") + var_description = typer.prompt( "Description", default=f"Value for {var_name}" ) @@ -849,16 +815,16 @@ def make( included_files.sort() excluded_files.sort() - click.echo("\nFiles that will be included in the template:") + console.print("\nFiles that will be included in the template:") for file in included_files: - click.echo(f" + {file}") + console.print(f" + {file}") # Show which Markdown files will be templated md_files = [f for f in included_files if f.lower().endswith(".md")] if md_files: - click.echo("\nMarkdown files that will be converted to Jinja templates:") + console.print("\nMarkdown files that will be converted to Jinja templates:") for file in md_files: - click.echo(f" * {file}") + console.print(f" * {file}") # Show which KiCad project files will be templated kicad_files = [ @@ -867,7 +833,7 @@ def make( if f.lower().endswith((".kicad_pro", ".kicad_sch", ".kicad_pcb")) ] if kicad_files: - click.echo("\nKiCad project files that will be templated:") + console.print("\nKiCad project files that will be templated:") for file in kicad_files: # Show the templated filename that will be used if file.lower().endswith(".kicad_pro"): @@ -879,21 +845,22 @@ def make( else: templated_name = file - click.echo(f" * {file} → {templated_name}") + console.print(f" * {file} → {templated_name}") - click.echo("\nFiles that will be excluded from the template:") + console.print("\nFiles that will be excluded from the template:") for file in excluded_files: - click.echo(f" - {file}") + console.print(f" - {file}") # If this is a dry run, stop here if dry_run: - click.echo("\nDry run complete. No changes were made.") + console.print("\nDry run complete. No changes were made.") return # Create the template try: # Create the template directory structure - Path(output_directory).mkdir(parents=True, exist_ok=True) + if output_directory: + output_directory.mkdir(parents=True, exist_ok=True) # Create template structure with special handling for Markdown files create_template_structure( @@ -904,62 +871,54 @@ def make( additional_excludes=exclude or None, ) - click.echo(f"\nTemplate '{name}' created successfully at {output_directory}") + console.print(f"\nTemplate '{name}' created successfully at {output_directory}") # Add hints for next steps - click.echo("\nNext steps:") - click.echo( - f"1. Edit {output_directory / TEMPLATE_METADATA} to customize template metadata" + console.print("\nNext steps:") + console.print( + f"1. Edit {output_directory / TEMPLATE_METADATA if output_directory else 'N/A'} to customize template metadata" ) - click.echo( - f"2. Customize template files in {output_directory / TEMPLATE_CONTENT_DIR}" + console.print( + f"2. Customize template files in {output_directory / TEMPLATE_CONTENT_DIR if output_directory else 'N/A'}" ) - click.echo( - f"3. Edit post-creation hook in {output_directory / HOOKS_DIR / POST_CREATE_HOOK} if needed" + console.print( + f"3. Edit post-creation hook in {output_directory / HOOKS_DIR / POST_CREATE_HOOK if output_directory else 'N/A'} if needed" ) - click.echo( + console.print( f"4. Use your template with: kilm template create MyProject --template {name}" ) # Add information about filename templating syntax - click.echo("\nFilename Templating:") - click.echo("For Windows compatibility, use %{variable} syntax in filenames:") - click.echo(" - %{project_name}.kicad_pro") - click.echo(" - %{project_name.lower}.kicad_sch") - click.echo(" - %{project_name.replace(' ', '-')}.kicad_pcb") - click.echo(" - %{project_name.upper.replace(' ', '_')}.md") - click.echo( + console.print("\nFilename Templating:") + console.print("For Windows compatibility, use %{variable} syntax in filenames:") + console.print(" - %{project_name}.kicad_pro") + console.print(" - %{project_name.lower}.kicad_sch") + console.print(" - %{project_name.replace(' ', '-')}.kicad_pcb") + console.print(" - %{project_name.upper.replace(' ', '_')}.md") + console.print( "(Old {{variable}} syntax still works but may cause issues on Windows)" ) except Exception as e: - click.echo(f"Error creating template: {str(e)}", err=True) + err_console.print(f"Error creating template: {str(e)}") traceback.print_exc() sys.exit(1) -@template.command() -@click.option( - "--library", - help="Filter templates by library name", - default=None, -) -@click.option( - "--verbose", - "-v", - is_flag=True, - default=False, - help="Show detailed information including variables", - show_default=True, -) -@click.option( - "--json", - is_flag=True, - default=False, - help="Output in JSON format", - show_default=True, -) -def list_templates_cmd(library, verbose, json): +def list_templates( + library: Annotated[ + Optional[str], typer.Option(help="Filter templates by library name") + ] = None, + verbose: Annotated[ + bool, + typer.Option( + "-v", "--verbose", help="Show detailed information including variables" + ), + ] = False, + json_output: Annotated[ + bool, typer.Option("--json", help="Output in JSON format") + ] = False, +) -> None: """List all available templates. Displays all available templates across all configured libraries, @@ -989,8 +948,17 @@ def list_templates_cmd(library, verbose, json): all_templates = find_all_templates(config) if not all_templates: - click.echo("No templates found in any configured libraries.") - click.echo("Use 'kilm template make' to create a template first.") + console.print() + console.print( + Panel( + "[yellow]No templates found in any configured libraries.[/yellow]\n\n" + "[cyan]💡 Get Started:[/cyan]\n" + "• Create a template: [blue]kilm template make[/blue]\n" + "• Check library configuration: [blue]kilm list-libraries[/blue]", + title="[bold yellow]⚠️ No Templates[/bold yellow]", + border_style="yellow", + ) + ) return # Filter by library if requested @@ -1002,14 +970,25 @@ def list_templates_cmd(library, verbose, json): } if not all_templates: - click.echo(f"No templates found in library '{library}'.") + console.print() + console.print( + Panel( + f"[yellow]No templates found in library '[cyan]{library}[/cyan]'.[/yellow]\n\n" + "[cyan]💡 Try:[/cyan]\n" + f"• List all libraries: [blue]kilm list-libraries[/blue]\n" + f"• Create template in '{library}': [blue]kilm template make --library {library}[/blue]\n" + "• List all templates: [blue]kilm template list[/blue]", + title="[bold yellow]⚠️ No Templates Found[/bold yellow]", + border_style="yellow", + ) + ) return # If JSON output is requested - if json: + if json_output: import json as json_lib - click.echo(json_lib.dumps(all_templates, indent=2)) + console.print(json_lib.dumps(all_templates, indent=2)) return # Group templates by library for display @@ -1020,61 +999,85 @@ def list_templates_cmd(library, verbose, json): templates_by_library[lib_name] = [] templates_by_library[lib_name].append((name, data)) - # Display templates - click.echo("Available Templates:") - click.echo("===================") - click.echo() + # Display templates in Rich tables organized by library + console.print() for lib_name, templates in templates_by_library.items(): - click.echo(f"Library: {lib_name}") - click.echo("-" * (len(lib_name) + 9)) + # Create a table for each library + table = Table( + title=f"[bold cyan]{lib_name}[/bold cyan] Templates", + show_header=True, + header_style="bold magenta", + border_style="cyan", + ) + + table.add_column("Template", style="cyan", no_wrap=True) + table.add_column("Version", justify="center", style="blue", width=8) + table.add_column("Description", style="green") + + if verbose: + table.add_column("Variables", style="yellow", max_width=30) + table.add_column("Extends", style="magenta", max_width=15) + # Add rows for each template for name, data in sorted(templates): description = data.get("description", "No description") - use_case = data.get("use_case", "") version = data.get("version", "1.0.0") + extends = data.get("extends", "") - click.echo() - click.echo(f"- {name} (v{version})") - click.echo(f" Description: {description}") + row_data = [f"[bold]{name}[/bold]", version, description] - if use_case: - click.echo(f" Use Case: {use_case}") - - if data.get("extends"): - click.echo(f" Extends: {data.get('extends')}") - - # Show variables if verbose if verbose: + # Format variables for display variables = data.get("variables", {}) + var_display = "" if variables: - click.echo() - click.echo(" Variables:") - for var_name, var_info in variables.items(): - var_desc = var_info.get("description", "") - var_default = var_info.get("default", "") - click.echo( - f" {var_name}: {var_desc} (default: '{var_default}')" - ) + var_list = [f"{k}" for k in variables] + var_display = ", ".join(var_list[:3]) # Show first 3 variables + if len(var_list) > 3: + var_display += f", +{len(var_list) - 3} more" + else: + var_display = "[dim]None[/dim]" - # Show dependencies if present - dependencies = data.get("dependencies", {}) - if dependencies: - recommended = dependencies.get("recommended", []) - if recommended: - click.echo() - click.echo(" Recommended Dependencies:") - for dep in recommended: - click.echo(f" - {dep}") + row_data.append(var_display) + row_data.append(extends if extends else "[dim]None[/dim]") - click.echo("") # Empty line between templates + table.add_row(*row_data) - click.echo("") # Empty line between libraries + console.print(table) + console.print() - # Show count - click.echo(f"Total: {len(all_templates)} template(s) found") + # Add usage hint panel + hint_content = ( + "[cyan]Usage:[/cyan] kilm template create --template \n" + ) + hint_content += "[cyan]Verbose:[/cyan] kilm template list --verbose\n" + hint_content += "[cyan]Filter:[/cyan] kilm template list --library " + + console.print( + Panel( + hint_content, + title="[bold blue]💡 Quick Tips[/bold blue]", + border_style="blue", + ) + ) + # Add summary panel + library_count = len(templates_by_library) + template_count = len(all_templates) + + summary_content = f"[green]Libraries:[/green] {library_count}\n" + summary_content += f"[green]Templates:[/green] {template_count}" + + console.print( + Panel( + summary_content, + title="[bold cyan]📊 Summary[/bold cyan]", + border_style="cyan", + width=30, + ) + ) # Register the template command if __name__ == "__main__": - template() + main_callback() diff --git a/kicad_lib_manager/commands/unpin/__init__.py b/kicad_lib_manager/commands/unpin/__init__.py index 90604f2..788b895 100644 --- a/kicad_lib_manager/commands/unpin/__init__.py +++ b/kicad_lib_manager/commands/unpin/__init__.py @@ -1,3 +1,14 @@ +import typer + from .command import unpin -__all__ = ["unpin"] +# Create Typer app and add the unpin command as callback +unpin_app = typer.Typer( + name="unpin", + help="Unpin libraries in KiCad for quick access", + rich_markup_mode="rich", + callback=unpin, + invoke_without_command=True, +) + +__all__ = ["unpin", "unpin_app"] diff --git a/kicad_lib_manager/commands/unpin/command.py b/kicad_lib_manager/commands/unpin/command.py index 0db172c..040347e 100644 --- a/kicad_lib_manager/commands/unpin/command.py +++ b/kicad_lib_manager/commands/unpin/command.py @@ -1,76 +1,75 @@ """ -Unpin command implementation for KiCad Library Manager. +Unpin command implementation for KiCad Library Manager (Typer version). """ import json -import sys +from typing import Optional -import click +import typer +from rich.console import Console +from rich.panel import Panel -from ...library_manager import find_kicad_config +from ...services.library_service import LibraryService from ...utils.backup import create_backup - -@click.command() -@click.option( - "--symbols", - "-s", - multiple=True, - help="Symbol libraries to unpin (can be specified multiple times)", -) -@click.option( - "--footprints", - "-f", - multiple=True, - help="Footprint libraries to unpin (can be specified multiple times)", -) -@click.option( - "--all", - is_flag=True, - help="Unpin all libraries", -) -@click.option( - "--dry-run", - is_flag=True, - help="Show what would be done without making changes", -) -@click.option( - "--max-backups", - default=5, - show_default=True, - help="Maximum number of backups to keep", -) -@click.option( - "--verbose", - "-v", - is_flag=True, - help="Show verbose output for debugging", -) -def unpin(symbols, footprints, all, dry_run, max_backups, verbose): +console = Console() + + +def unpin( + symbols: Optional[list[str]] = typer.Option( + None, + "--symbols", + "-s", + help="Symbol libraries to unpin (can be specified multiple times)", + ), + footprints: Optional[list[str]] = typer.Option( + None, + "--footprints", + "-f", + help="Footprint libraries to unpin (can be specified multiple times)", + ), + all_libraries: bool = typer.Option(False, "--all", help="Unpin all libraries"), + dry_run: bool = typer.Option( + False, "--dry-run", help="Show what would be done without making changes" + ), + max_backups: int = typer.Option( + 5, "--max-backups", help="Maximum number of backups to keep" + ), + verbose: bool = typer.Option( + False, "--verbose", "-v", help="Show verbose output for debugging" + ), +) -> None: """Unpin libraries in KiCad""" + # Handle None values from optional parameters + symbols = symbols or [] + footprints = footprints or [] + # Enforce mutual exclusivity of --all with --symbols/--footprints - if all and (symbols or footprints): - raise click.UsageError( - "'--all' cannot be used with '--symbols' or '--footprints'" + if all_libraries and (symbols or footprints): + console.print( + "[red]Error: '--all' cannot be used with '--symbols' or '--footprints'[/red]" ) + raise typer.Exit(1) # Find KiCad configuration try: - kicad_config = find_kicad_config() + kicad_config = LibraryService.find_kicad_config() if verbose: - click.echo(f"Found KiCad configuration at: {kicad_config}") + console.print(f"Found KiCad configuration at: [blue]{kicad_config}[/blue]") except Exception as e: - click.echo(f"Error finding KiCad configuration: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error finding KiCad configuration: {e}[/red]") + raise typer.Exit(1) from e # Get the kicad_common.json file kicad_common = kicad_config / "kicad_common.json" if not kicad_common.exists(): - click.echo("KiCad common configuration file not found, nothing to unpin") + console.print( + "[yellow]KiCad common configuration file not found, nothing to unpin[/yellow]" + ) return # If --all is specified, unpin all libraries - if all: + if all_libraries: try: with kicad_common.open() as f: config = json.load(f) @@ -82,38 +81,39 @@ def unpin(symbols, footprints, all, dry_run, max_backups, verbose): if verbose: if symbols: - click.echo(f"Found {len(symbols)} pinned symbol libraries") + console.print( + f"Found [cyan]{len(symbols)}[/cyan] pinned symbol libraries" + ) if footprints: - click.echo( - f"Found {len(footprints)} pinned footprint libraries" + console.print( + f"Found [cyan]{len(footprints)}[/cyan] pinned footprint libraries" ) else: - click.echo( - "No session information found in KiCad configuration, nothing to unpin" + console.print( + "[yellow]No session information found in KiCad configuration, nothing to unpin[/yellow]" ) return if not symbols and not footprints: - click.echo("No pinned libraries found, nothing to unpin") + console.print( + "[yellow]No pinned libraries found, nothing to unpin[/yellow]" + ) return except Exception as e: - click.echo(f"Error reading pinned libraries: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error reading pinned libraries: {e}[/red]") + raise typer.Exit(1) from e # If no libraries are specified, print an error - if not symbols and not footprints and not all: - click.echo("Error: No libraries specified to unpin", err=True) - click.echo( - "Use --symbols, --footprints, or --all to specify libraries to unpin", - err=True, + if not symbols and not footprints and not all_libraries: + console.print("[red]Error: No libraries specified to unpin[/red]") + console.print( + "[yellow]Use --symbols, --footprints, or --all to specify libraries to unpin[/yellow]" ) - sys.exit(1) + raise typer.Exit(1) - # Convert tuples to lists if needed - if isinstance(symbols, tuple): - symbols = list(symbols) - if isinstance(footprints, tuple): - footprints = list(footprints) + # Ensure we have lists (Typer already handles this) + symbols = symbols or [] + footprints = footprints or [] # Unpin the libraries by removing them from the kicad_common.json file try: @@ -124,8 +124,8 @@ def unpin(symbols, footprints, all, dry_run, max_backups, verbose): # Ensure session section exists if "session" not in config: - click.echo( - "No session information found in KiCad configuration, nothing to unpin" + console.print( + "[yellow]No session information found in KiCad configuration, nothing to unpin[/yellow]" ) return @@ -161,29 +161,36 @@ def unpin(symbols, footprints, all, dry_run, max_backups, verbose): if changes_needed: if dry_run: - click.echo( - f"Would unpin {len(symbols) if symbols else 0} symbol and {len(footprints) if footprints else 0} footprint libraries in KiCad" + console.print( + f"[yellow]Would unpin {len(symbols) if symbols else 0} symbol and {len(footprints) if footprints else 0} footprint libraries in KiCad[/yellow]" ) else: - click.echo( - f"Unpinned {len(symbols) if symbols else 0} symbol and {len(footprints) if footprints else 0} footprint libraries in KiCad" + success_msg = f"[green]Unpinned {len(symbols) if symbols else 0} symbol and {len(footprints) if footprints else 0} footprint libraries in KiCad[/green]" + console.print( + Panel( + f"{success_msg}\n\n" + f"[blue]• Created backup of kicad_common.json[/blue]\n" + f"[yellow]• Restart KiCad for changes to take effect[/yellow]", + title="✅ Libraries Unpinned", + border_style="green", + ) ) - click.echo("Created backup of kicad_common.json") - click.echo("Restart KiCad for changes to take effect") else: - click.echo("No changes needed, libraries already unpinned in KiCad") + console.print( + "[yellow]No changes needed, libraries already unpinned in KiCad[/yellow]" + ) if verbose: if symbols: - click.echo("\nUnpinned Symbol Libraries:") + console.print("\n[bold cyan]Unpinned Symbol Libraries:[/bold cyan]") for symbol in sorted(symbols): - click.echo(f" - {symbol}") + console.print(f" • [cyan]{symbol}[/cyan]") if footprints: - click.echo("\nUnpinned Footprint Libraries:") + console.print("\n[bold cyan]Unpinned Footprint Libraries:[/bold cyan]") for footprint in sorted(footprints): - click.echo(f" - {footprint}") + console.print(f" • [cyan]{footprint}[/cyan]") except Exception as e: - click.echo(f"Error unpinning libraries: {e}", err=True) - sys.exit(1) + console.print(f"[red]Error unpinning libraries: {e}[/red]") + raise typer.Exit(1) from e diff --git a/kicad_lib_manager/commands/update/__init__.py b/kicad_lib_manager/commands/update/__init__.py index 1ed481f..a1fc882 100644 --- a/kicad_lib_manager/commands/update/__init__.py +++ b/kicad_lib_manager/commands/update/__init__.py @@ -1,3 +1,13 @@ +import typer + from .command import update -__all__ = ["update"] +update_app = typer.Typer( + name="update", + help="Update KiLM itself to the latest version", + rich_markup_mode="rich", + callback=update, + invoke_without_command=True, +) + +__all__ = ["update", "update_app"] diff --git a/kicad_lib_manager/commands/update/command.py b/kicad_lib_manager/commands/update/command.py index 7f8c764..570d33d 100644 --- a/kicad_lib_manager/commands/update/command.py +++ b/kicad_lib_manager/commands/update/command.py @@ -1,31 +1,28 @@ """ -Update command implementation for KiCad Library Manager. +Update command implementation for KiCad Library Manager (Typer version). Updates KiLM itself to the latest version. """ import importlib.metadata +from typing import Annotated -import click - -from ...auto_update import UpdateManager - - -@click.command() -@click.option( - "--check", - is_flag=True, - default=False, - help="Check for updates without installing", - show_default=True, -) -@click.option( - "--force", - is_flag=True, - default=False, - help="Force update even if already up to date", - show_default=True, -) -def update(check, force): +import typer +from rich.console import Console +from rich.panel import Panel + +from ...services.update_service import UpdateManager + +console = Console() + + +def update( + check: Annotated[ + bool, typer.Option(help="Check for updates without installing") + ] = False, + force: Annotated[ + bool, typer.Option(help="Force update even if already up to date") + ] = False, +) -> None: """Update KiLM to the latest version. This command updates KiLM itself by downloading and installing the latest @@ -39,59 +36,77 @@ def update(check, force): Use --check to see if updates are available without installing. """ - # Display deprecation notice prominently - click.echo("\n" + "=" * 70) - click.echo("⚠️ BREAKING CHANGE NOTICE (KiLM 0.4.0)") - click.echo("=" * 70) - click.echo("The 'kilm update' command now updates KiLM itself.") - click.echo("To update library content, use 'kilm sync' instead.") - click.echo("This notice will be removed in a future version.") - click.echo("=" * 70 + "\n") + + deprecation_notice = ( + "[bold yellow]⚠️ BREAKING CHANGE NOTICE (KiLM 0.4.0)[/bold yellow]\n\n" + "The [bold]kilm update[/bold] command now updates KiLM itself.\n" + "To update library content, use [bold cyan]kilm sync[/bold cyan] instead.\n" + "This notice will be removed in a future version." + ) + console.print(Panel(deprecation_notice, expand=False, border_style="yellow")) version = importlib.metadata.version("kilm") update_manager = UpdateManager(version) - click.echo(f"Current KiLM version: {version}") - click.echo(f"Installation method: {update_manager.installation_method}") - click.echo("\nChecking for updates...") + console.print( + f"[blue]Current KiLM version:[/blue] [bold cyan]v{version}[/bold cyan]" + ) + console.print( + f"[blue]Installation method:[/blue] {update_manager.installation_method}" + ) + console.print("\n[bold cyan]Checking for updates...[/bold cyan]") latest_version = update_manager.check_latest_version() if latest_version is None: - click.echo("Could not check for updates. Please try again later.") + console.print("[red]Could not check for updates. Please try again later.[/red]") return if not update_manager.is_newer_version_available(latest_version): if not force: - click.echo(f"KiLM is up to date (v{version})") + console.print( + f"[green]KiLM is up to date[/green] [bold green]v{version}[/bold green]" + ) return else: - click.echo(f"Forcing update to v{latest_version} (current: v{version})") + console.print( + f"[yellow]Forcing update to v{latest_version}[/yellow] (current: v{version})" + ) else: - click.echo(f"New version available: {latest_version}") + console.print( + f"[green]New version available:[/green] [bold green]v{latest_version}[/bold green]" + ) if check: if update_manager.is_newer_version_available(latest_version): - click.echo(f"\nUpdate available: {latest_version}") - click.echo(f"To update, run: {update_manager.get_update_instruction()}") + console.print( + f"\n[green]Update available:[/green] [bold green]v{latest_version}[/bold green]" + ) + console.print( + f"[blue]To update, run:[/blue] [cyan]{update_manager.get_update_instruction()}[/cyan]" + ) else: - click.echo("No updates available.") + console.print("[green]No updates available[/green]") return # Perform the update if update_manager.can_auto_update(): - click.echo(f"\nUpdating KiLM to version {latest_version}...") + console.print( + f"\n[bold cyan]Updating KiLM to version {latest_version}...[/bold cyan]" + ) success, message = update_manager.perform_update() if success: - click.echo(f"✅ {message}") - click.echo(f"KiLM has been updated to version {latest_version}") + console.print(f"[bold green]✅ {message}[/bold green]") + console.print( + f"[green]KiLM has been updated to version {latest_version}[/green]" + ) else: - click.echo(f"❌ {message}") + console.print(f"[bold red]❌ {message}[/bold red]") else: instruction = update_manager.get_update_instruction() - click.echo( - f"\nManual update required for {update_manager.installation_method} installation." + console.print( + f"\n[yellow]Manual update required for {update_manager.installation_method} installation.[/yellow]" ) - click.echo(f"Please run: {instruction}") + console.print(f"[blue]Please run:[/blue] [cyan]{instruction}[/cyan]") diff --git a/kicad_lib_manager/commands/update/docs.mdx b/kicad_lib_manager/commands/update/docs.mdx index 186d0de..151064c 100644 --- a/kicad_lib_manager/commands/update/docs.mdx +++ b/kicad_lib_manager/commands/update/docs.mdx @@ -72,18 +72,6 @@ kilm update kilm update --force ``` -## Configuration - -Update checking behavior can be configured using `kilm config`: - -```bash -# Disable update checking -kilm config set update_check false - -# Set check frequency -kilm config set update_check_frequency weekly # daily, weekly, never -``` - ## Troubleshooting **Update fails with permission errors:** diff --git a/kicad_lib_manager/interfaces/__init__.py b/kicad_lib_manager/interfaces/__init__.py new file mode 100644 index 0000000..9f5a2aa --- /dev/null +++ b/kicad_lib_manager/interfaces/__init__.py @@ -0,0 +1 @@ +"""Service interfaces for KiCad Library Manager.""" diff --git a/kicad_lib_manager/interfaces/config_service.py b/kicad_lib_manager/interfaces/config_service.py new file mode 100644 index 0000000..6258620 --- /dev/null +++ b/kicad_lib_manager/interfaces/config_service.py @@ -0,0 +1,55 @@ +""" +Configuration service protocol interface for KiCad Library Manager. +""" + +from abc import abstractmethod +from pathlib import Path +from typing import Optional, Protocol + + +class ConfigServiceProtocol(Protocol): + """Protocol for configuration management services.""" + + @abstractmethod + def get_config_file_path(self) -> Path: + """Get the path to the KiLM configuration file.""" + + @abstractmethod + def load_config(self) -> dict: + """Load the KiLM configuration.""" + + @abstractmethod + def save_config(self, config: dict) -> None: + """Save the KiLM configuration.""" + + @abstractmethod + def add_library(self, name: str, path: str, library_type: str) -> None: + """Add a library to the configuration.""" + + @abstractmethod + def remove_library(self, name: str) -> None: + """Remove a library from the configuration.""" + + @abstractmethod + def get_libraries(self) -> list[dict]: + """Get all configured libraries.""" + + @abstractmethod + def get_library_by_name(self, name: str) -> Optional[dict]: + """Get a specific library by name.""" + + @abstractmethod + def get_current_library(self) -> Optional[str]: + """Get the current active library path.""" + + @abstractmethod + def set_current_library(self, path: str) -> None: + """Set the current active library.""" + + @abstractmethod + def get_max_backups(self) -> int: + """Get the maximum number of backups to keep.""" + + @abstractmethod + def set_max_backups(self, count: int) -> None: + """Set the maximum number of backups to keep.""" diff --git a/kicad_lib_manager/interfaces/kicad_service.py b/kicad_lib_manager/interfaces/kicad_service.py new file mode 100644 index 0000000..556944e --- /dev/null +++ b/kicad_lib_manager/interfaces/kicad_service.py @@ -0,0 +1,60 @@ +""" +KiCad service protocol interface for KiCad Library Manager. +""" + +from abc import abstractmethod +from pathlib import Path +from typing import Optional, Protocol + + +class KiCadServiceProtocol(Protocol): + """Protocol for KiCad configuration and management services.""" + + @abstractmethod + def find_kicad_config_dir(self) -> Path: + """Find the KiCad configuration directory.""" + + @abstractmethod + def get_environment_variables(self, config_dir: Path) -> dict[str, str]: + """Get KiCad environment variables from configuration.""" + + @abstractmethod + def set_environment_variables( + self, + config_dir: Path, + env_vars: dict[str, str], + backup: bool = True, + max_backups: int = 5, + ) -> bool: + """Set KiCad environment variables.""" + + @abstractmethod + def get_configured_libraries( + self, config_dir: Path + ) -> tuple[list[dict], list[dict]]: + """ + Get configured symbol and footprint libraries. + + Returns: + Tuple of (symbol_libraries, footprint_libraries) + """ + + @abstractmethod + def add_libraries_to_kicad( + self, + config_dir: Path, + symbol_libs: Optional[list[dict]] = None, + footprint_libs: Optional[list[dict]] = None, + backup: bool = True, + max_backups: int = 5, + ) -> bool: + """Add libraries to KiCad library tables.""" + + @abstractmethod + def get_pinned_libraries(self, config_dir: Path) -> tuple[list[str], list[str]]: + """ + Get pinned libraries from KiCad configuration. + + Returns: + Tuple of (symbol_libraries, footprint_libraries) + """ diff --git a/kicad_lib_manager/interfaces/library_service.py b/kicad_lib_manager/interfaces/library_service.py new file mode 100644 index 0000000..ff04f07 --- /dev/null +++ b/kicad_lib_manager/interfaces/library_service.py @@ -0,0 +1,75 @@ +""" +Library service protocol interface for KiCad Library Manager. +""" + +from abc import abstractmethod +from pathlib import Path +from typing import Optional, Protocol, Union + + +class LibraryServiceProtocol(Protocol): + """Protocol for library management services.""" + + @abstractmethod + def list_libraries(self, directory: Path) -> tuple[list[str], list[str]]: + """ + List symbol and footprint libraries in a directory. + + Returns: + Tuple of (symbol_libraries, footprint_libraries) + """ + ... + + @abstractmethod + def initialize_library( + self, + directory: Path, + name: Optional[str] = None, + description: Optional[str] = None, + env_var: Optional[str] = None, + force: bool = False, + no_env_var: bool = False, + ) -> dict[str, Union[str, bool, dict[str, bool]]]: + """Initialize a library in the given directory.""" + ... + + @abstractmethod + def get_library_metadata( + self, directory: Path + ) -> Optional[dict[str, Union[str, bool, dict[str, bool]]]]: + """Get metadata for a library directory.""" + ... + + @abstractmethod + def pin_libraries( + self, + symbol_libs: list[str], + footprint_libs: list[str], + kicad_config_dir: Path, + dry_run: bool = False, + max_backups: int = 5, + ) -> bool: + """ + Pin libraries in KiCad for quick access. + + Returns: + True if changes were made, False otherwise + """ + ... + + @abstractmethod + def unpin_libraries( + self, + symbol_libs: list[str], + footprint_libs: list[str], + kicad_config_dir: Path, + dry_run: bool = False, + max_backups: int = 5, + ) -> bool: + """ + Unpin libraries in KiCad. + + Returns: + True if changes were made, False otherwise + """ + ... diff --git a/kicad_lib_manager/library_manager.py b/kicad_lib_manager/library_manager.py deleted file mode 100644 index 8e0b9ed..0000000 --- a/kicad_lib_manager/library_manager.py +++ /dev/null @@ -1,405 +0,0 @@ -""" -Core functionality for managing KiCad libraries -""" - -import os -import platform -from pathlib import Path -from typing import Dict, List, Optional, Set, Tuple - -import yaml - -from .utils.env_vars import expand_user_path -from .utils.file_ops import ( - list_configured_libraries, - list_libraries, - validate_lib_table, -) - - -def find_kicad_config() -> Path: - """ - Find the KiCad configuration directory for the current platform - - Returns: - Path to the KiCad configuration directory - - Raises: - FileNotFoundError: If KiCad configuration directory is not found - """ - system = platform.system() - - if system == "Darwin": # macOS - config_dir = Path.home() / "Library" / "Preferences" / "kicad" - elif system == "Linux": - config_dir = Path.home() / ".config" / "kicad" - elif system == "Windows": - appdata = os.environ.get("APPDATA") - if not appdata: - raise FileNotFoundError("APPDATA environment variable not found") - config_dir = Path(appdata) / "kicad" - else: - raise FileNotFoundError(f"Unsupported platform: {system}") - - if not config_dir.exists(): - raise FileNotFoundError( - f"KiCad configuration directory not found at {config_dir}. " - "Please run KiCad at least once before using this tool." - ) - - # Find the most recent KiCad version directory - version_dirs = [d for d in config_dir.iterdir() if d.is_dir()] - if not version_dirs: - raise FileNotFoundError( - f"No KiCad version directories found in {config_dir}. " - "Please run KiCad at least once before using this tool." - ) - - # Sort directories by version number (assuming directories with numbers are version dirs) - version_dirs = [d for d in version_dirs if any(c.isdigit() for c in d.name)] - if not version_dirs: - raise FileNotFoundError( - f"No KiCad version directories found in {config_dir}. " - "Please run KiCad at least once before using this tool." - ) - - latest_dir = sorted(version_dirs, key=lambda d: d.name)[-1] - - # Check for required files - sym_table = latest_dir / "sym-lib-table" - fp_table = latest_dir / "fp-lib-table" - - if not sym_table.exists() and not fp_table.exists(): - raise FileNotFoundError( - f"KiCad library tables not found in {latest_dir}. " - "Please run KiCad at least once before using this tool." - ) - - return latest_dir - - -def get_library_description(lib_type: str, lib_name: str, kicad_lib_dir: str) -> str: - """ - Get a description for a library from the YAML file or generate a default one - - Args: - lib_type: Either 'symbols' or 'footprints' - lib_name: The name of the library - kicad_lib_dir: The KiCad library directory - - Returns: - A description for the library - """ - yaml_file = Path(kicad_lib_dir) / "library_descriptions.yaml" - - # Check if YAML file exists - if yaml_file.exists(): - try: - with yaml_file.open() as f: - data = yaml.safe_load(f) - - if ( - data - and isinstance(data, dict) - and lib_type in data - and isinstance(data[lib_type], dict) - and lib_name in data[lib_type] - ): - return data[lib_type][lib_name] - except Exception: - pass - - # Default description if YAML file doesn't exist or doesn't contain the library - if lib_type == "symbols": - return f"{lib_name} symbol library" - else: - return f"{lib_name} footprint library" - - -def format_uri(base_path: str, lib_name: str, lib_type: str) -> str: - """ - Format a URI for a KiCad library. - - Args: - base_path: The base path to the library directory - lib_name: The name of the library - lib_type: The type of library (symbols or footprints) - - Returns: - The formatted URI string - - Raises: - ValueError: If base_path is empty, lib_type is invalid, or path format is invalid - """ - if not base_path: - raise ValueError("Base path cannot be empty") - - if lib_type not in ["symbols", "footprints"]: - raise ValueError(f"Invalid library type: {lib_type}") - - # Validate ${...} format if present - if base_path.startswith("${") and not base_path.endswith("}"): - raise ValueError(f"Invalid environment variable format: {base_path}") - - # Helper function to check if a path is absolute - def is_absolute_path(path: str) -> bool: - return ( - path.startswith("/") # Unix-style - or path.startswith("\\") # Windows-style with backslash - or (len(path) > 1 and path[1] == ":") # Windows-style with drive letter - ) - - # Normalize path separators to forward slashes first - base_path = base_path.replace("\\", "/") - - # Check if the path is already in ${...} format - if base_path.startswith("${") and base_path.endswith("}"): - # Extract the path from inside the curly braces - path = base_path[2:-1] - if is_absolute_path(path): - # If it's an absolute path, remove the ${...} wrapper - base_path = path - # Otherwise, keep the ${...} wrapper for environment variables - else: - # If it's not in ${...} format, check if it's an absolute path - if not is_absolute_path(base_path): - # If it's not an absolute path, treat it as an environment variable - base_path = f"${{{base_path}}}" - - # Construct the URI based on library type - if lib_type == "symbols": - return f"{base_path}/symbols/{lib_name}.kicad_sym" - else: - return f"{base_path}/footprints/{lib_name}.pretty" - - -def add_libraries( - kicad_lib_dir: str, - kicad_config: Path, - kicad_3d_dir: Optional[str] = None, - additional_3d_dirs: Optional[Dict[str, str]] = None, - dry_run: bool = False, -) -> Tuple[Set[str], bool]: - """ - Add KiCad libraries to the configuration. - - Args: - kicad_lib_dir: Path to the KiCad library directory - kicad_config: Path to the KiCad configuration directory - kicad_3d_dir: Path to the KiCad 3D models directory (optional) - additional_3d_dirs: Dictionary of additional 3D model directories with their - environment variable names as keys (optional) - dry_run: If True, don't actually make any changes - - Returns: - Tuple of (set of libraries added, whether changes were needed) - - Raises: - FileNotFoundError: If the library directory does not exist - ValueError: If the library directory does not contain symbols or footprints - """ - # Check if library directory exists - # First expand any environment variables in the path - if kicad_lib_dir.startswith("${") and kicad_lib_dir.endswith("}"): - env_var = kicad_lib_dir[2:-1] - if env_var in os.environ: - kicad_lib_dir = os.environ[env_var] - else: - raise FileNotFoundError(f"Environment variable {env_var} not found") - # Check if it's NOT an absolute path (Unix/Windows) or UNC path (Windows) - # and treat it as a potential environment variable name if it's not. - elif not ( - kicad_lib_dir.startswith("/") - or ( - len(kicad_lib_dir) > 2 and kicad_lib_dir[1] == ":" - ) # Check for C: style paths - or (kicad_lib_dir.startswith("\\\\")) # Check for UNC paths like \\server\share - ): - # If it's an environment variable name without ${} - if kicad_lib_dir in os.environ: - kicad_lib_dir = os.environ[kicad_lib_dir] - else: - raise FileNotFoundError(f"Environment variable {kicad_lib_dir} not found") - - # Now expand any user paths - kicad_lib_dir = expand_user_path(kicad_lib_dir) - lib_dir = Path(kicad_lib_dir) - if not lib_dir.exists(): - raise FileNotFoundError(f"KiCad library directory not found: {kicad_lib_dir}") - - # Check if 3D models directory exists - if kicad_3d_dir: - kicad_3d_dir = expand_user_path(kicad_3d_dir) - models_dir = Path(kicad_3d_dir) - if not models_dir.exists(): - raise FileNotFoundError( - f"KiCad 3D models directory not found: {kicad_3d_dir}" - ) - - # Check if additional 3D model directories exist - all_3d_dirs = {} - if kicad_3d_dir: - all_3d_dirs["KICAD_3D_LIB"] = kicad_3d_dir - - if additional_3d_dirs: - for env_var, path in additional_3d_dirs.items(): - path = expand_user_path(path) - dir_path = Path(path) - if not dir_path.exists(): - print(f"Warning: 3D models directory not found: {path} (skipping)") - continue - - # Only add if this is a different path than the main 3D models directory - if kicad_3d_dir and os.path.normpath(path) == os.path.normpath( - kicad_3d_dir - ): - continue - - all_3d_dirs[env_var] = path - - # Get list of available libraries - symbols, footprints = list_libraries(kicad_lib_dir) - if not symbols and not footprints: - raise ValueError(f"No libraries found in {kicad_lib_dir}") - - # Check if library tables exist - sym_table = kicad_config / "sym-lib-table" - fp_table = kicad_config / "fp-lib-table" - - # Get existing libraries - sym_libs, fp_libs = list_configured_libraries(kicad_config) - sym_lib_names = {lib["name"] for lib in sym_libs} - fp_lib_names = {lib["name"] for lib in fp_libs} - - # Check for new libraries - new_symbols = [lib for lib in symbols if lib not in sym_lib_names] - new_footprints = [lib for lib in footprints if lib not in fp_lib_names] - - # Generate variable references for 3D model paths - env_var_refs = {} - for env_var, path in all_3d_dirs.items(): - # Convert to format KiCad expects: ${ENV_VAR} - env_var_refs[path] = f"${{{env_var}}}" - - # Add new libraries to symbol table - sym_changes_needed = False - new_sym_entries = [] - for lib in new_symbols: - uri = format_uri(kicad_lib_dir, lib, "symbols") - - # Add the library with UTF-8 encoded description - new_sym_entries.append( - { - "name": lib, - "uri": uri, - "options": "", - "description": get_library_description("symbols", lib, kicad_lib_dir) - .encode("utf-8") - .decode("utf-8"), - } - ) - sym_changes_needed = True - - # Add new libraries to footprint table - fp_changes_needed = False - new_fp_entries = [] - for lib in new_footprints: - uri = format_uri(kicad_lib_dir, lib, "footprints") - - # Add the library with UTF-8 encoded description - new_fp_entries.append( - { - "name": lib, - "uri": uri, - "options": "", - "description": get_library_description("footprints", lib, kicad_lib_dir) - .encode("utf-8") - .decode("utf-8"), - } - ) - fp_changes_needed = True - - # Only make changes if needed - changes_needed = sym_changes_needed or fp_changes_needed - if changes_needed and not dry_run: - # Add new entries to symbol table - if sym_changes_needed: - add_entries_to_table(sym_table, new_sym_entries) - - # Add new entries to footprint table - if fp_changes_needed: - add_entries_to_table(fp_table, new_fp_entries) - - # Return the set of added libraries - added_libraries = set(new_symbols + new_footprints) - return added_libraries, changes_needed - - -def add_entries_to_table(table_path: Path, entries: List[Dict[str, str]]) -> None: - """ - Add entries to a KiCad library table file - - Args: - table_path: Path to the library table - entries: List of entries to add - """ - # Make sure the table exists and has a valid format - validate_lib_table(table_path, False) - - # Read existing content, ensuring UTF-8 encoding - with table_path.open(encoding="utf-8") as f: - content = f.read() - - # Find the last proper closing parenthesis of the table - closing_paren_index = -1 - lines = content.splitlines() - for i in range(len(lines) - 1, -1, -1): - if lines[i].strip() == ")": - closing_paren_index = i - break - - if closing_paren_index == -1: - raise ValueError( - f"Could not find closing parenthesis in library table: {table_path}" - ) - - # Insert new entries before the closing parenthesis - new_content = "" - for i, line in enumerate(lines): - if i == closing_paren_index: - # Insert entries before the closing parenthesis line - for entry in entries: - # Process the URI to make sure it's properly formatted - uri = entry["uri"] - - # Check if URI starts with ${/ or ${\ - this indicates an improperly formatted path - if uri.startswith("${/") or uri.startswith("${\\"): - # Extract the path from inside the curly braces using a more robust method - try: - # Find the first { and last } - start = uri.find("{") - end = uri.rfind("}") - if start != -1 and end != -1 and end > start: - path = uri[start + 1 : end] - # Replace with the actual path without environment variable syntax - uri = path + uri[end + 1 :] - except Exception: - # If there's any error in processing, keep the original URI - pass - - # Format the entry with proper escaping - entry_str = ( - f" (lib " - f'(name "{entry["name"]}")' - f'(type "KiCad")' - f'(uri "{uri}")' - f'(options "{entry["options"]}")' - f'(descr "{entry["description"]}"))\n' - ) - new_content += entry_str - - new_content += line + "\n" - - # Write updated content, ensuring UTF-8 encoding - with table_path.open("w", encoding="utf-8") as f: - f.write(new_content) diff --git a/kicad_lib_manager/main.py b/kicad_lib_manager/main.py new file mode 100644 index 0000000..f6d1ce1 --- /dev/null +++ b/kicad_lib_manager/main.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +""" +Main Typer CLI entry point for KiCad Library Manager +""" + +import importlib.metadata +from typing import Annotated, Optional + +import typer +from rich.align import Align +from rich.console import Console +from rich.text import Text +from rich.traceback import install +from typer.core import TyperGroup + +from .commands.add_3d import add_3d_app +from .commands.add_hook import add_hook_app +from .commands.config import config_app +from .commands.init import init_app +from .commands.list_libraries import list_app +from .commands.pin import pin_app +from .commands.setup import setup_app +from .commands.status import status_app +from .commands.sync import sync_app +from .commands.template import template_app +from .commands.unpin import unpin_app +from .commands.update import update_app +from .utils.banner import show_banner + +TAGLINE = "Professional KiCad library management" + +# Install rich traceback handler for better error display +install(show_locals=True) + +# Initialize Rich console +console = Console() + + +class BannerGroup(TyperGroup): + """Custom group that shows banner before help.""" + + def format_help(self, ctx, formatter): + # Show banner before help + show_banner(console, justify="center") + console.print() + console.print(Align.center(Text(TAGLINE, style="italic bright_yellow"))) + console.print() + super().format_help(ctx, formatter) + + +# Create main Typer app +app = typer.Typer( + name="kilm", + help="KiCad Library Manager - Manage KiCad libraries across projects and workstations", + context_settings={"help_option_names": ["-h", "--help"]}, + rich_markup_mode="rich", + pretty_exceptions_enable=True, + pretty_exceptions_show_locals=False, + invoke_without_command=True, + cls=BannerGroup, +) + + +def version_callback(value: bool) -> None: + """Print version information and exit.""" + if value: + show_banner(console, justify="left") + console.print() + + version = importlib.metadata.version("kilm") + console.print(f"KiCad Library Manager (KiLM) version [cyan]{version}[/cyan]") + raise typer.Exit() + + +@app.callback() +def main( + _version: Annotated[ + Optional[bool], + typer.Option( + "--version", + "-V", + callback=version_callback, + is_eager=True, + help="Show version information and exit", + ), + ] = None, +) -> None: + """ + [bold blue]KiCad Library Manager[/bold blue] - Professional KiCad library management + + This tool helps you configure and manage KiCad libraries across your projects + and workstations with a modern, type-safe CLI interface. + + [bold]Common Commands:[/bold] + • [cyan]kilm status[/cyan] - Show current configuration + • [cyan]kilm setup[/cyan] - Configure KiCad to use libraries + • [cyan]kilm list[/cyan] - List available libraries + • [cyan]kilm sync[/cyan] - Update library content + """ + # Show banner when no arguments are provided, centered + import sys + + if not sys.argv[1:]: + show_banner(console) + console.print() + console.print(Align.center(Text(TAGLINE, style="italic bright_yellow"))) + console.print() + console.print( + "[dim]Use 'kilm --help' to see all commands.[/dim]", justify="center" + ) + + +# Register command apps (migrated to Typer) +app.add_typer(status_app, name="status", help="Show current library configuration") +app.add_typer(list_app, name="list", help="List available KiCad libraries") +app.add_typer(init_app, name="init", help="Initialize library configuration") +app.add_typer(pin_app, name="pin", help="Pin favorite libraries") +app.add_typer(unpin_app, name="unpin", help="Unpin favorite libraries") +app.add_typer(setup_app, name="setup", help="Configure KiCad to use libraries") +app.add_typer(config_app, name="config", help="Manage configuration settings") +app.add_typer(template_app, name="template", help="Manage project templates") +app.add_typer(add_3d_app, name="add-3d", help="Add 3D model libraries") +app.add_typer(sync_app, name="sync", help="Update/sync library content") +app.add_typer(update_app, name="update", help="Update KiLM itself") +app.add_typer(add_hook_app, name="add-hook", help="Add project hooks") + + +if __name__ == "__main__": + app() diff --git a/kicad_lib_manager/services/__init__.py b/kicad_lib_manager/services/__init__.py new file mode 100644 index 0000000..f8c2b40 --- /dev/null +++ b/kicad_lib_manager/services/__init__.py @@ -0,0 +1 @@ +"""Service implementations for KiCad Library Manager.""" diff --git a/kicad_lib_manager/config.py b/kicad_lib_manager/services/config_service.py similarity index 84% rename from kicad_lib_manager/config.py rename to kicad_lib_manager/services/config_service.py index 76123f5..0b829e0 100644 --- a/kicad_lib_manager/config.py +++ b/kicad_lib_manager/services/config_service.py @@ -1,15 +1,15 @@ """ -Configuration management +Configuration management for KiCad Library Manager. """ import time from pathlib import Path -from typing import Dict, List, Optional, Tuple, TypedDict, Union, cast +from typing import Optional, TypedDict, Union, cast import click import yaml -from .constants import ( +from ..utils.constants import ( CONFIG_DIR_NAME, CONFIG_FILE_NAME, DEFAULT_LIBRARIES, @@ -25,9 +25,9 @@ class LibraryDict(TypedDict): type: str -ConfigValue = Union[str, int, List[LibraryDict]] +ConfigValue = Union[str, int, list[LibraryDict]] -DEFAULT_CONFIG: Dict[str, ConfigValue] = { +DEFAULT_CONFIG: dict[str, ConfigValue] = { "max_backups": DEFAULT_MAX_BACKUPS, "libraries": DEFAULT_LIBRARIES, "update_check": True, @@ -138,7 +138,7 @@ def remove_library(self, name: str, library_type: Optional[str] = None) -> bool: return removed - def get_libraries(self, library_type: Optional[str] = None) -> List[LibraryDict]: + def get_libraries(self, library_type: Optional[str] = None) -> list[LibraryDict]: """ Get libraries from configuration @@ -176,7 +176,7 @@ def get_library_path( return None - def get_current_library_paths(self) -> Tuple[Optional[str], Optional[str]]: + def get_current_library_paths(self) -> tuple[Optional[str], Optional[str]]: """ Get paths for the current active libraries @@ -276,7 +276,7 @@ def _normalize_libraries_field(self) -> None: return # Validate and clean up each library entry - normalized_libraries: List[LibraryDict] = [] + normalized_libraries: list[LibraryDict] = [] for lib in libraries: validated_lib = _validate_library_entry(lib) if validated_lib is not None: @@ -286,7 +286,7 @@ def _normalize_libraries_field(self) -> None: self._config["libraries"] = normalized_libraries - def _get_normalized_libraries(self) -> List[LibraryDict]: + def _get_normalized_libraries(self) -> list[LibraryDict]: """ Get libraries ensuring they are properly normalized. @@ -305,7 +305,7 @@ def _get_normalized_libraries(self) -> List[LibraryDict]: return [] # Validate and normalize each library entry - normalized_libraries: List[LibraryDict] = [] + normalized_libraries: list[LibraryDict] = [] needs_save = False for lib in libraries_raw: @@ -365,7 +365,7 @@ def mark_update_check_performed(self) -> None: last_check_file = cache_dir / "last_update_check" last_check_file.write_text(str(time.time())) - def get_update_preferences(self) -> Dict[str, Union[bool, str]]: + def get_update_preferences(self) -> dict[str, Union[bool, str]]: """ Get update-related preferences. @@ -452,7 +452,7 @@ def _make_library_dict(name: str, path: str, type_: str) -> LibraryDict: def _validate_library_entry( - lib: Union[Dict[str, str], LibraryDict], + lib: Union[dict[str, str], LibraryDict], ) -> Optional[LibraryDict]: """Validate and normalize a library entry.""" if isinstance(lib, dict) and all(key in lib for key in ["name", "path", "type"]): @@ -462,3 +462,64 @@ def _validate_library_entry( type_=str(lib["type"]), ) return None + + +class ConfigService: + """Service wrapper for managing KiLM configuration.""" + + def __init__(self): + """Initialize the configuration service.""" + self._config = Config() + + def get_config_file_path(self) -> Path: + """Get the path to the KiLM configuration file.""" + return self._config._get_config_file() + + def load_config(self) -> dict: + """Load the KiLM configuration.""" + return self._config._config + + def save_config(self, config: dict) -> None: + """Save the KiLM configuration.""" + self._config._config = config + self._config.save() + + def add_library(self, name: str, path: str, library_type: str) -> None: + """Add a library to the configuration.""" + self._config.add_library(name, path, library_type) + + def remove_library(self, name: str) -> None: + """Remove a library from the configuration.""" + self._config.remove_library(name) + + def get_libraries(self) -> list[LibraryDict]: + """Get all configured libraries.""" + return self._config.get_libraries() + + def get_library_by_name(self, name: str) -> Optional[LibraryDict]: + """Get a specific library by name.""" + libraries = self.get_libraries() + for lib in libraries: + if lib.get("name") == name: + return lib + return None + + def get_current_library(self) -> Optional[str]: + """Get the current active library path.""" + return self._config.get_current_library() + + def set_current_library(self, path: str) -> None: + """Set the current active library.""" + self._config.set_current_library(path) + + def get_max_backups(self) -> int: + """Get the maximum number of backups to keep.""" + value = self._config.get("max_backups", DEFAULT_MAX_BACKUPS) + if isinstance(value, int): + return value + return DEFAULT_MAX_BACKUPS + + def set_max_backups(self, count: int) -> None: + """Set the maximum number of backups to keep.""" + self._config.set("max_backups", count) + self._config.save() diff --git a/kicad_lib_manager/services/kicad_service.py b/kicad_lib_manager/services/kicad_service.py new file mode 100644 index 0000000..4295491 --- /dev/null +++ b/kicad_lib_manager/services/kicad_service.py @@ -0,0 +1,103 @@ +""" +KiCad service implementation for KiCad Library Manager. +""" + +import json +from pathlib import Path +from typing import Optional + +from ..utils.file_ops import list_configured_libraries +from .library_service import LibraryService + + +class KiCadService: + """Service for managing KiCad configuration and libraries.""" + + @staticmethod + def find_kicad_config_dir() -> Path: + return LibraryService.find_kicad_config() + + def get_environment_variables(self, config_dir: Path) -> dict[str, str]: + """Get KiCad environment variables from configuration.""" + kicad_common = config_dir / "kicad_common.json" + + if not kicad_common.exists(): + return {} + + try: + with kicad_common.open() as f: + config = json.load(f) + + if "environment" in config and "vars" in config["environment"]: + return config["environment"]["vars"] + + except Exception: + pass + + return {} + + def set_environment_variables( + self, + config_dir: Path, + env_vars: dict[str, str], + backup: bool = True, + max_backups: int = 5, + ) -> bool: + """Set KiCad environment variables.""" + # This would use the existing update_kicad_env_vars function + from ..utils.env_vars import update_kicad_env_vars + + return update_kicad_env_vars(config_dir, env_vars, backup, max_backups) + + def get_configured_libraries( + self, config_dir: Path + ) -> tuple[list[dict], list[dict]]: + """Get configured symbol and footprint libraries.""" + return list_configured_libraries(config_dir) + + def add_libraries_to_kicad( + self, + config_dir: Path, + symbol_libs: Optional[list[dict]] = None, + footprint_libs: Optional[list[dict]] = None, + backup: bool = True, + max_backups: int = 5, + ) -> bool: + """Add libraries to KiCad library tables.""" + # This would use the existing add_libraries function + from .library_service import LibraryService + + # TODO: Properly implement library addition with symbol_libs and footprint_libs + _ = symbol_libs, footprint_libs, max_backups # Suppress unused warnings + added_libs, changes_needed = LibraryService.add_libraries( + str(config_dir), + config_dir, + dry_run=not backup, + ) + _ = added_libs # Suppress unused warning + return changes_needed + + def get_pinned_libraries(self, config_dir: Path) -> tuple[list[str], list[str]]: + """Get pinned libraries from KiCad configuration.""" + kicad_common = config_dir / "kicad_common.json" + + if not kicad_common.exists(): + return [], [] + + try: + with kicad_common.open() as f: + config = json.load(f) + + symbol_libs = [] + footprint_libs = [] + + if "session" in config: + if "pinned_symbol_libs" in config["session"]: + symbol_libs = config["session"]["pinned_symbol_libs"] + if "pinned_fp_libs" in config["session"]: + footprint_libs = config["session"]["pinned_fp_libs"] + + return symbol_libs, footprint_libs + + except Exception: + return [], [] diff --git a/kicad_lib_manager/services/library_service.py b/kicad_lib_manager/services/library_service.py new file mode 100644 index 0000000..47bdeda --- /dev/null +++ b/kicad_lib_manager/services/library_service.py @@ -0,0 +1,544 @@ +""" +Library service implementation for KiCad Library Manager. +Core functionality for managing KiCad libraries. +""" + +import os +import platform +from pathlib import Path +from typing import Optional + +import yaml + +from ..utils.env_vars import expand_user_path, update_pinned_libraries +from ..utils.file_ops import ( + list_configured_libraries, + list_libraries, + validate_lib_table, +) +from ..utils.metadata import ( + generate_env_var_name, + get_default_github_metadata, + read_github_metadata, + write_github_metadata, +) + + +class LibraryService: + """Service for managing KiCad libraries.""" + + def list_libraries(self, directory: Path) -> tuple[list[str], list[str]]: + """List symbol and footprint libraries in a directory.""" + return list_libraries(str(directory)) + + def initialize_library( + self, + directory: Path, + name: Optional[str] = None, + description: Optional[str] = None, + env_var: Optional[str] = None, + force: bool = False, + no_env_var: bool = False, + ) -> dict: + """Initialize a library in the given directory.""" + # Check for existing metadata + metadata = read_github_metadata(directory) + + if metadata and not force: + # Use existing metadata with potential overrides + if name: + metadata["name"] = name + if description: + metadata["description"] = description + if env_var: + metadata["env_var"] = env_var + elif no_env_var and "env_var" in metadata: + del metadata["env_var"] + + if any([name, description, env_var, no_env_var]): + metadata["updated_with"] = "kilm" + write_github_metadata(directory, metadata) + else: + # Create new metadata + metadata = get_default_github_metadata(directory) + + if name: + metadata["name"] = name + if not env_var and not no_env_var: + metadata["env_var"] = generate_env_var_name(name, "KICAD_LIB") + + if description: + metadata["description"] = description + + if env_var: + metadata["env_var"] = env_var + elif no_env_var and "env_var" in metadata: + del metadata["env_var"] + + write_github_metadata(directory, metadata) + + # Create library directory structure + self._create_library_structure(directory) + + # Update capabilities + capabilities: dict[str, bool] = { + "symbols": (directory / "symbols").exists(), + "footprints": (directory / "footprints").exists(), + "templates": (directory / "templates").exists(), + } + metadata["capabilities"] = capabilities + write_github_metadata(directory, metadata) + + return metadata + + def get_library_metadata(self, directory: Path) -> Optional[dict]: + """Get metadata for a library directory.""" + return read_github_metadata(directory) + + def pin_libraries( + self, + symbol_libs: list[str], + footprint_libs: list[str], + kicad_config_dir: Path, + dry_run: bool = False, + max_backups: int = 5, + ) -> bool: + """Pin libraries in KiCad for quick access.""" + return update_pinned_libraries( + kicad_config_dir, + symbol_libs=symbol_libs, + footprint_libs=footprint_libs, + dry_run=dry_run, + max_backups=max_backups, + ) + + def unpin_libraries( + self, + symbol_libs: list[str], + footprint_libs: list[str], + kicad_config_dir: Path, + dry_run: bool = False, + max_backups: int = 5, + ) -> bool: + """Unpin libraries in KiCad.""" + # This would need to be implemented in utils/env_vars.py + # For now, we'll return False as it's not implemented + # TODO: Implement unpin functionality + _ = symbol_libs, footprint_libs, kicad_config_dir, dry_run, max_backups + return False + + def _create_library_structure(self, directory: Path) -> tuple[list[str], list[str]]: + """Create the required directory structure for a library.""" + required_folders = ["symbols", "footprints", "templates"] + existing_folders = [] + created_folders = [] + + for folder in required_folders: + folder_path = directory / folder + if folder_path.exists(): + existing_folders.append(folder) + else: + folder_path.mkdir(parents=True, exist_ok=True) + created_folders.append(folder) + + return existing_folders, created_folders + + @staticmethod + def add_libraries( + kicad_lib_dir: str, + kicad_config: Path, + kicad_3d_dir: Optional[str] = None, + additional_3d_dirs: Optional[dict[str, str]] = None, + dry_run: bool = False, + ) -> tuple[set[str], bool]: + """ + Add KiCad libraries to the configuration. + + Args: + kicad_lib_dir: Path to the KiCad library directory + kicad_config: Path to the KiCad configuration directory + kicad_3d_dir: Path to the KiCad 3D models directory (optional) + additional_3d_dirs: Dictionary of additional 3D model directories with their + environment variable names as keys (optional) + dry_run: If True, don't actually make any changes + + Returns: + Tuple of (set of libraries added, whether changes were needed) + + Raises: + FileNotFoundError: If the library directory does not exist + ValueError: If the library directory does not contain symbols or footprints + """ + # Check if library directory exists + # First expand any environment variables in the path + if kicad_lib_dir.startswith("${") and kicad_lib_dir.endswith("}"): + env_var = kicad_lib_dir[2:-1] + if env_var in os.environ: + kicad_lib_dir = os.environ[env_var] + else: + raise FileNotFoundError(f"Environment variable {env_var} not found") + # Check if it's NOT an absolute path (Unix/Windows) or UNC path (Windows) + # and treat it as a potential environment variable name if it's not. + elif not ( + kicad_lib_dir.startswith("/") + or ( + len(kicad_lib_dir) > 2 and kicad_lib_dir[1] == ":" + ) # Check for C: style paths + or ( + kicad_lib_dir.startswith("\\\\") + ) # Check for UNC paths like \\server\share + ): + # If it's an environment variable name without ${} + if kicad_lib_dir in os.environ: + kicad_lib_dir = os.environ[kicad_lib_dir] + else: + raise FileNotFoundError( + f"Environment variable {kicad_lib_dir} not found" + ) + + # Now expand any user paths + kicad_lib_dir = expand_user_path(kicad_lib_dir) + lib_dir = Path(kicad_lib_dir) + if not lib_dir.exists(): + raise FileNotFoundError( + f"KiCad library directory not found: {kicad_lib_dir}" + ) + + # Check if 3D models directory exists + if kicad_3d_dir: + kicad_3d_dir = expand_user_path(kicad_3d_dir) + models_dir = Path(kicad_3d_dir) + if not models_dir.exists(): + raise FileNotFoundError( + f"KiCad 3D models directory not found: {kicad_3d_dir}" + ) + + # Check if additional 3D model directories exist + all_3d_dirs = {} + if kicad_3d_dir: + all_3d_dirs["KICAD_3D_LIB"] = kicad_3d_dir + + if additional_3d_dirs: + for env_var, path in additional_3d_dirs.items(): + path = expand_user_path(path) + dir_path = Path(path) + if not dir_path.exists(): + print(f"Warning: 3D models directory not found: {path} (skipping)") + continue + + # Only add if this is a different path than the main 3D models directory + if kicad_3d_dir and os.path.normpath(path) == os.path.normpath( + kicad_3d_dir + ): + continue + + all_3d_dirs[env_var] = path + + # Get list of available libraries + symbols, footprints = list_libraries(kicad_lib_dir) + if not symbols and not footprints: + raise ValueError(f"No libraries found in {kicad_lib_dir}") + + # Check if library tables exist + sym_table = kicad_config / "sym-lib-table" + fp_table = kicad_config / "fp-lib-table" + + # Get existing libraries + sym_libs, fp_libs = list_configured_libraries(kicad_config) + sym_lib_names = {lib["name"] for lib in sym_libs} + fp_lib_names = {lib["name"] for lib in fp_libs} + + # Check for new libraries + new_symbols = [lib for lib in symbols if lib not in sym_lib_names] + new_footprints = [lib for lib in footprints if lib not in fp_lib_names] + + # Generate variable references for 3D model paths + env_var_refs = {} + for env_var, path in all_3d_dirs.items(): + # Convert to format KiCad expects: ${ENV_VAR} + env_var_refs[path] = f"${{{env_var}}}" + + # Add new libraries to symbol table + sym_changes_needed = False + new_sym_entries = [] + for lib in new_symbols: + uri = LibraryService.format_uri(kicad_lib_dir, lib, "symbols") + + # Add the library with UTF-8 encoded description + new_sym_entries.append( + { + "name": lib, + "uri": uri, + "options": "", + "description": LibraryService.get_library_description( + "symbols", lib, kicad_lib_dir + ) + .encode("utf-8") + .decode("utf-8"), + } + ) + sym_changes_needed = True + + # Add new libraries to footprint table + fp_changes_needed = False + new_fp_entries = [] + for lib in new_footprints: + uri = LibraryService.format_uri(kicad_lib_dir, lib, "footprints") + + # Add the library with UTF-8 encoded description + new_fp_entries.append( + { + "name": lib, + "uri": uri, + "options": "", + "description": LibraryService.get_library_description( + "footprints", lib, kicad_lib_dir + ) + .encode("utf-8") + .decode("utf-8"), + } + ) + fp_changes_needed = True + + # Only make changes if needed + changes_needed = sym_changes_needed or fp_changes_needed + if changes_needed and not dry_run: + # Add new entries to symbol table + if sym_changes_needed: + LibraryService.add_entries_to_table(sym_table, new_sym_entries) + + # Add new entries to footprint table + if fp_changes_needed: + LibraryService.add_entries_to_table(fp_table, new_fp_entries) + + # Return the set of added libraries + added_libraries = set(new_symbols + new_footprints) + return added_libraries, changes_needed + + @staticmethod + def find_kicad_config() -> Path: + """ + Find the KiCad configuration directory for the current platform + + Returns: + Path to the KiCad configuration directory + + Raises: + FileNotFoundError: If KiCad configuration directory is not found + """ + system = platform.system() + + if system == "Darwin": # macOS + config_dir = Path.home() / "Library" / "Preferences" / "kicad" + elif system == "Linux": + config_dir = Path.home() / ".config" / "kicad" + elif system == "Windows": + appdata = os.environ.get("APPDATA") + if not appdata: + raise FileNotFoundError("APPDATA environment variable not found") + config_dir = Path(appdata) / "kicad" + else: + raise FileNotFoundError(f"Unsupported platform: {system}") + + if not config_dir.exists(): + raise FileNotFoundError( + f"KiCad configuration directory not found at {config_dir}. " + "Please run KiCad at least once before using this tool." + ) + + # Find the most recent KiCad version directory + version_dirs = [d for d in config_dir.iterdir() if d.is_dir()] + if not version_dirs: + raise FileNotFoundError( + f"No KiCad version directories found in {config_dir}. " + "Please run KiCad at least once before using this tool." + ) + + # Sort directories by version number (assuming directories with numbers are version dirs) + version_dirs = [d for d in version_dirs if any(c.isdigit() for c in d.name)] + if not version_dirs: + raise FileNotFoundError( + f"No KiCad version directories found in {config_dir}. " + "Please run KiCad at least once before using this tool." + ) + + latest_dir = sorted(version_dirs, key=lambda d: d.name)[-1] + + # Check for required files + sym_table = latest_dir / "sym-lib-table" + fp_table = latest_dir / "fp-lib-table" + + if not sym_table.exists() and not fp_table.exists(): + raise FileNotFoundError( + f"KiCad library tables not found in {latest_dir}. " + "Please run KiCad at least once before using this tool." + ) + + return latest_dir + + @staticmethod + def get_library_description( + lib_type: str, lib_name: str, kicad_lib_dir: str + ) -> str: + """ + Get a description for a library from the YAML file or generate a default one + + Args: + lib_type: Either 'symbols' or 'footprints' + lib_name: The name of the library + kicad_lib_dir: The KiCad library directory + + Returns: + A description for the library + """ + yaml_file = Path(kicad_lib_dir) / "library_descriptions.yaml" + + # Check if YAML file exists + if yaml_file.exists(): + try: + with yaml_file.open() as f: + data = yaml.safe_load(f) + + if ( + data + and isinstance(data, dict) + and lib_type in data + and isinstance(data[lib_type], dict) + and lib_name in data[lib_type] + ): + return data[lib_type][lib_name] + except Exception: + pass + + # Default description if YAML file doesn't exist or doesn't contain the library + if lib_type == "symbols": + return f"{lib_name} symbol library" + else: + return f"{lib_name} footprint library" + + @staticmethod + def format_uri(base_path: str, lib_name: str, lib_type: str) -> str: + """ + Format a URI for a KiCad library. + + Args: + base_path: The base path to the library directory + lib_name: The name of the library + lib_type: The type of library (symbols or footprints) + + Returns: + The formatted URI string + + Raises: + ValueError: If base_path is empty, lib_type is invalid, or path format is invalid + """ + if not base_path: + raise ValueError("Base path cannot be empty") + + if lib_type not in ["symbols", "footprints"]: + raise ValueError(f"Invalid library type: {lib_type}") + + # Validate ${...} format if present + if base_path.startswith("${") and not base_path.endswith("}"): + raise ValueError(f"Invalid environment variable format: {base_path}") + + # Helper function to check if a path is absolute + def is_absolute_path(path: str) -> bool: + return ( + path.startswith("/") # Unix-style + or path.startswith("\\") # Windows-style with backslash + or (len(path) > 1 and path[1] == ":") # Windows-style with drive letter + ) + + # Normalize path separators to forward slashes first + base_path = base_path.replace("\\", "/") + + # Check if the path is already in ${...} format + if base_path.startswith("${") and base_path.endswith("}"): + # Extract the path from inside the curly braces + path = base_path[2:-1] + if is_absolute_path(path): + # If it's an absolute path, remove the ${...} wrapper + base_path = path + # Otherwise, keep the ${...} wrapper for environment variables + else: + # If it's not in ${...} format, check if it's an absolute path + if not is_absolute_path(base_path): + # If it's not an absolute path, treat it as an environment variable + base_path = f"${{{base_path}}}" + + # Construct the URI based on library type + if lib_type == "symbols": + return f"{base_path}/symbols/{lib_name}.kicad_sym" + else: + return f"{base_path}/footprints/{lib_name}.pretty" + + @staticmethod + def add_entries_to_table(table_path: Path, entries: list[dict[str, str]]) -> None: + """ + Add entries to a KiCad library table file + + Args: + table_path: Path to the library table + entries: List of entries to add + """ + # Make sure the table exists and has a valid format + validate_lib_table(table_path, False) + + # Read existing content, ensuring UTF-8 encoding + with table_path.open(encoding="utf-8") as f: + content = f.read() + + # Find the last proper closing parenthesis of the table + closing_paren_index = -1 + lines = content.splitlines() + for i in range(len(lines) - 1, -1, -1): + if lines[i].strip() == ")": + closing_paren_index = i + break + + if closing_paren_index == -1: + raise ValueError( + f"Could not find closing parenthesis in library table: {table_path}" + ) + + # Insert new entries before the closing parenthesis + new_content = "" + for i, line in enumerate(lines): + if i == closing_paren_index: + # Insert entries before the closing parenthesis line + for entry in entries: + # Process the URI to make sure it's properly formatted + uri = entry["uri"] + + # Check if URI starts with ${/ or ${\ - this indicates an improperly formatted path + if uri.startswith("${/") or uri.startswith("${\\"): + # Extract the path from inside the curly braces using a more robust method + try: + # Find the first { and last } + start = uri.find("{") + end = uri.rfind("}") + if start != -1 and end != -1 and end > start: + path = uri[start + 1 : end] + # Replace with the actual path without environment variable syntax + uri = path + uri[end + 1 :] + except Exception: + # If there's any error in processing, keep the original URI + pass + + # Format the entry with proper escaping + entry_str = ( + f" (lib " + f'(name "{entry["name"]}")' + f'(type "KiCad")' + f'(uri "{uri}")' + f'(options "{entry["options"]}")' + f'(descr "{entry["description"]}"))\n' + ) + new_content += entry_str + + new_content += line + "\n" + + # Write updated content, ensuring UTF-8 encoding + with table_path.open("w", encoding="utf-8") as f: + f.write(new_content) diff --git a/kicad_lib_manager/auto_update.py b/kicad_lib_manager/services/update_service.py similarity index 86% rename from kicad_lib_manager/auto_update.py rename to kicad_lib_manager/services/update_service.py index 87ef5d5..7378120 100644 --- a/kicad_lib_manager/auto_update.py +++ b/kicad_lib_manager/services/update_service.py @@ -9,7 +9,7 @@ import sys import time from pathlib import Path -from typing import Dict, Optional, Tuple +from typing import Optional import requests from packaging.version import InvalidVersion, Version @@ -101,7 +101,7 @@ def check_latest_version(self) -> Optional[str]: return None - def _load_cache(self) -> Optional[Dict]: + def _load_cache(self) -> Optional[dict]: """Load cached version data.""" if self.cache_file.exists(): try: @@ -114,7 +114,7 @@ def _load_cache(self) -> Optional[Dict]: pass return None - def _save_cache(self, data: Dict): + def _save_cache(self, data: dict): """Save version data to cache.""" self.cache_file.parent.mkdir(parents=True, exist_ok=True) with Path(self.cache_file).open("w") as f: @@ -198,7 +198,7 @@ def can_auto_update(self) -> bool: """Check if automatic update is possible for this installation method.""" return self.installation_method in ["pipx", "pip", "pip-venv", "uv"] - def perform_update(self) -> Tuple[bool, str]: + def perform_update(self) -> tuple[bool, str]: """ Execute update using detected installation method. Returns: (success: bool, message: str) @@ -228,3 +228,34 @@ def perform_update(self) -> Tuple[bool, str]: else: instruction = self.get_update_instruction() return False, f"Unsupported installation method. Run: {instruction}" + + +class UpdateService: + """Service wrapper for KiLM update functionality.""" + + def __init__(self, current_version: str): + self.manager = UpdateManager(current_version) + + def check_for_updates(self) -> Optional[str]: + """Check for available updates.""" + return self.manager.check_latest_version() + + def is_update_available(self, latest_version: str) -> bool: + """Check if an update is available.""" + return self.manager.is_newer_version_available(latest_version) + + def get_installation_method(self) -> str: + """Get detected installation method.""" + return self.manager.installation_method + + def get_update_instructions(self) -> str: + """Get update instructions for current installation method.""" + return self.manager.get_update_instruction() + + def can_auto_update(self) -> bool: + """Check if automatic updates are supported.""" + return self.manager.can_auto_update() + + def perform_update(self) -> tuple[bool, str]: + """Perform the update.""" + return self.manager.perform_update() diff --git a/kicad_lib_manager/utils/__init__.py b/kicad_lib_manager/utils/__init__.py index 53101f1..c6a00df 100644 --- a/kicad_lib_manager/utils/__init__.py +++ b/kicad_lib_manager/utils/__init__.py @@ -1,3 +1,7 @@ """ Utility functions for KiCad Library Manager """ + +from .banner import show_banner + +__all__ = ["show_banner"] diff --git a/kicad_lib_manager/utils/banner.py b/kicad_lib_manager/utils/banner.py new file mode 100644 index 0000000..82f1f41 --- /dev/null +++ b/kicad_lib_manager/utils/banner.py @@ -0,0 +1,68 @@ +""" +Banner utilities for KiCad Library Manager +""" + +from typing import Literal + +from rich.console import Console + + +def show_banner( + console: Console, justify: Literal["center", "left", "right"] = "center" +) -> None: + """ + Display the KiLM banner with colorful ASCII art. + + Front characters (KILM text) and back characters (box drawing) use different colors + to create a layered visual effect. + + Args: + console: Rich console instance to print the banner to + """ + banner_lines = [ + "██╗ ██╗██╗██╗ ███╗ ███╗", + "██║ ██╔╝██║██║ ████╗ ████║", + "█████╔╝ ██║██║ ██╔████╔██║", + "██╔═██╗ ██║██║ ██║╚██╔╝██║", + "██║ ██╗██║███████╗██║ ╚═╝ ██║", + "╚═╝ ╚═╝╚═╝╚══════╝╚═╝ ╚═╝", + ] + + # Colors for front characters (KILM text) - gradient effect + front_colors = [ + "bright_blue", + "bright_blue", + "bright_cyan", + "bright_cyan", + "bright_green", + "bright_white", + ] + + # Colors for back characters (box drawing) - darker, more subtle + back_colors = [ + "white", + "white", + "white", + "white", + "white", + "white", + ] + + # Print each line with different colors for front and back characters + for line, front_color, back_color in zip(banner_lines, front_colors, back_colors): + # Split the line into front characters (KILM) and back characters (box drawing) + # The KILM text is represented by the block characters ██╗, ██║, etc. + # The back characters are the decorative box drawing elements + + # Only the solid block characters ██ are front characters + # All other characters (╗╔╝║═╚ and spaces) are back characters + colored_line = "" + for char in line: + if char == "█": + # Only the solid block character is the front character + colored_line += f"[bold {front_color}]{char}[/]" + else: + # All other characters are back characters + colored_line += f"[{back_color}]{char}[/]" + + console.print(colored_line, justify=justify) diff --git a/kicad_lib_manager/constants.py b/kicad_lib_manager/utils/constants.py similarity index 100% rename from kicad_lib_manager/constants.py rename to kicad_lib_manager/utils/constants.py diff --git a/kicad_lib_manager/utils/env_vars.py b/kicad_lib_manager/utils/env_vars.py index 2544a2a..a68620e 100644 --- a/kicad_lib_manager/utils/env_vars.py +++ b/kicad_lib_manager/utils/env_vars.py @@ -8,11 +8,11 @@ import shutil import subprocess from pathlib import Path -from typing import List, Optional +from typing import Optional # Import Config here, but only use it when needed to avoid circular imports try: - from ..config import Config + from ..services.config_service import Config except ImportError: Config = None @@ -230,8 +230,8 @@ def update_kicad_env_vars( def update_pinned_libraries( kicad_config: Path, - symbol_libs: Optional[List[str]] = None, - footprint_libs: Optional[List[str]] = None, + symbol_libs: Optional[list[str]] = None, + footprint_libs: Optional[list[str]] = None, dry_run: bool = False, max_backups: int = 5, ) -> bool: diff --git a/kicad_lib_manager/utils/file_ops.py b/kicad_lib_manager/utils/file_ops.py index 87ec2d5..f36eee6 100644 --- a/kicad_lib_manager/utils/file_ops.py +++ b/kicad_lib_manager/utils/file_ops.py @@ -4,11 +4,11 @@ import re from pathlib import Path -from typing import List, Optional, Tuple +from typing import Optional def read_file_with_encoding( - file_path: Path, encodings: Optional[List[str]] = None + file_path: Path, encodings: Optional[list[str]] = None ) -> str: """ Read a file trying multiple encodings until successful @@ -190,7 +190,7 @@ def add_footprint_lib( ) -def list_libraries(kicad_lib_dir: str) -> Tuple[List[str], List[str]]: +def list_libraries(kicad_lib_dir: str) -> tuple[list[str], list[str]]: """ List all available libraries in the repository @@ -224,7 +224,7 @@ def list_libraries(kicad_lib_dir: str) -> Tuple[List[str], List[str]]: return symbols, footprints -def list_configured_libraries(kicad_config: Path) -> Tuple[List[dict], List[dict]]: +def list_configured_libraries(kicad_config: Path) -> tuple[list[dict], list[dict]]: """ List all libraries currently configured in KiCad @@ -247,21 +247,26 @@ def list_configured_libraries(kicad_config: Path) -> Tuple[List[dict], List[dict if sym_table.exists(): content = read_file_with_encoding(sym_table) - # Extract all library entries - lib_entries = re.findall(r'\(lib \(name "([^"]+)"\)(.+?)\)', content, re.DOTALL) - for name, details in lib_entries: - lib_info = {"name": name} + # Match each complete (lib ...) entry on its own line + # This is more robust than trying to match up to the first ')' + for match in re.finditer(r"^\s*\(lib\s+(.*?)\)\s*$", content, re.MULTILINE): + entry = match.group(1) - # Extract other properties - uri_match = re.search(r'\(uri "([^"]+)"\)', details) + name_match = re.search(r'\(name\s+"([^"]+)"\)', entry) + if not name_match: + continue + lib_info = {"name": name_match.group(1)} + + # Extract other properties (support both uri and url just in case) + uri_match = re.search(r'\((?:uri|url)\s+"([^"]+)"\)', entry) if uri_match: lib_info["uri"] = uri_match.group(1) - type_match = re.search(r'\(type "([^"]+)"\)', details) + type_match = re.search(r'\(type\s+"([^"]+)"\)', entry) if type_match: lib_info["type"] = type_match.group(1) - descr_match = re.search(r'\(descr "([^"]+)"\)', details) + descr_match = re.search(r'\(descr\s+"([^"]+)"\)', entry) if descr_match: lib_info["description"] = descr_match.group(1) @@ -270,21 +275,23 @@ def list_configured_libraries(kicad_config: Path) -> Tuple[List[dict], List[dict if fp_table.exists(): content = read_file_with_encoding(fp_table) - # Extract all library entries - lib_entries = re.findall(r'\(lib \(name "([^"]+)"\)(.+?)\)', content, re.DOTALL) - for name, details in lib_entries: - lib_info = {"name": name} + for match in re.finditer(r"^\s*\(lib\s+(.*?)\)\s*$", content, re.MULTILINE): + entry = match.group(1) + + name_match = re.search(r'\(name\s+"([^"]+)"\)', entry) + if not name_match: + continue + lib_info = {"name": name_match.group(1)} - # Extract other properties - uri_match = re.search(r'\(uri "([^"]+)"\)', details) + uri_match = re.search(r'\((?:uri|url)\s+"([^"]+)"\)', entry) if uri_match: lib_info["uri"] = uri_match.group(1) - type_match = re.search(r'\(type "([^"]+)"\)', details) + type_match = re.search(r'\(type\s+"([^"]+)"\)', entry) if type_match: lib_info["type"] = type_match.group(1) - descr_match = re.search(r'\(descr "([^"]+)"\)', details) + descr_match = re.search(r'\(descr\s+"([^"]+)"\)', entry) if descr_match: lib_info["description"] = descr_match.group(1) diff --git a/kicad_lib_manager/utils/metadata.py b/kicad_lib_manager/utils/metadata.py index a0e5e4a..4c9b401 100644 --- a/kicad_lib_manager/utils/metadata.py +++ b/kicad_lib_manager/utils/metadata.py @@ -5,16 +5,16 @@ import json import re from pathlib import Path -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import yaml -from ..constants import CLOUD_METADATA_FILE, GITHUB_METADATA_FILE +from .constants import CLOUD_METADATA_FILE, GITHUB_METADATA_FILE def read_github_metadata( directory: Path, -) -> Optional[Dict[str, Union[str, List, Dict]]]: +) -> Optional[dict[str, Any]]: """ Read metadata from a GitHub library directory. @@ -41,7 +41,7 @@ def read_github_metadata( return None -def write_github_metadata(directory: Path, metadata: Dict[str, Any]) -> bool: +def write_github_metadata(directory: Path, metadata: dict[str, Any]) -> bool: """ Write metadata to a GitHub library directory. @@ -63,7 +63,7 @@ def write_github_metadata(directory: Path, metadata: Dict[str, Any]) -> bool: return False -def read_cloud_metadata(directory: Path) -> Optional[Dict[str, Any]]: +def read_cloud_metadata(directory: Path) -> Optional[dict[str, Union[str, int, None]]]: """ Read metadata from a cloud 3D model directory. @@ -90,7 +90,9 @@ def read_cloud_metadata(directory: Path) -> Optional[Dict[str, Any]]: return None -def write_cloud_metadata(directory: Path, metadata: Dict[str, Any]) -> bool: +def write_cloud_metadata( + directory: Path, metadata: dict[str, Union[str, int, None]] +) -> bool: """ Write metadata to a cloud 3D model directory. @@ -141,7 +143,9 @@ def generate_env_var_name(name: str, prefix: str = "") -> str: return clean_name -def get_default_github_metadata(directory: Path) -> Dict[str, Any]: +def get_default_github_metadata( + directory: Path, +) -> dict[str, Any]: """ Generate default metadata for a GitHub library. @@ -178,7 +182,7 @@ def get_default_github_metadata(directory: Path) -> Dict[str, Any]: } -def get_default_cloud_metadata(directory: Path) -> Dict[str, Any]: +def get_default_cloud_metadata(directory: Path) -> dict[str, Union[str, int, None]]: """ Generate default metadata for a cloud 3D model directory. diff --git a/kicad_lib_manager/utils/template.py b/kicad_lib_manager/utils/template.py index 6e32a32..e0a307c 100644 --- a/kicad_lib_manager/utils/template.py +++ b/kicad_lib_manager/utils/template.py @@ -9,15 +9,16 @@ import re import shutil import traceback +from collections.abc import Mapping from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any, Optional, TypedDict, Union, cast import click import jinja2 import pathspec import yaml -from ..constants import ( +from .constants import ( HOOKS_DIR, TEMPLATE_CONTENT_DIR, TEMPLATE_METADATA, @@ -49,6 +50,34 @@ FILENAME_VAR_PATTERN = re.compile(r"%\{([^}]+)\}") +class TemplateVariable(TypedDict): + """Template variable definition.""" + + description: str + default: str + + +class TemplateDependencies(TypedDict): + """Template dependencies definition.""" + + recommended: list[str] + + +class TemplateMetadata(TypedDict, total=False): + """Template metadata structure.""" + + name: str + description: str + use_case: str + version: str + variables: dict[str, TemplateVariable] + extends: str + dependencies: TemplateDependencies + path: str + source_library: str + library_path: str + + def get_gitignore_spec(directory: Path) -> Optional[pathspec.PathSpec]: """ Get a PathSpec object representing the gitignore patterns in the given directory. @@ -95,7 +124,7 @@ def get_gitignore_spec(directory: Path) -> Optional[pathspec.PathSpec]: return None -def list_templates_in_directory(directory: Path) -> List[Dict[str, Any]]: +def list_templates_in_directory(directory: Path) -> list[TemplateMetadata]: """ List all templates in a given directory. @@ -109,7 +138,7 @@ def list_templates_in_directory(directory: Path) -> List[Dict[str, Any]]: if not templates_dir.exists() or not templates_dir.is_dir(): return [] - templates = [] + templates: list[TemplateMetadata] = [] for template_dir in templates_dir.iterdir(): if not template_dir.is_dir(): @@ -130,7 +159,7 @@ def list_templates_in_directory(directory: Path) -> List[Dict[str, Any]]: metadata["path"] = str(template_dir) metadata["source_library"] = directory.name - templates.append(metadata) + templates.append(cast("TemplateMetadata", metadata)) except Exception as e: # Use click.echo for warnings/errors click.echo( @@ -141,8 +170,8 @@ def list_templates_in_directory(directory: Path) -> List[Dict[str, Any]]: def find_potential_variables( - directory: Path, patterns: Optional[List[str]] = None -) -> Dict[str, List[str]]: + directory: Path, patterns: Optional[list[str]] = None +) -> dict[str, list[str]]: """ Scan files in a directory for potential template variables. @@ -210,10 +239,10 @@ def create_template_metadata( name: str, description: Optional[str] = None, use_case: Optional[str] = None, - variables: Optional[Dict[str, Dict[str, Any]]] = None, + variables: Optional[dict[str, TemplateVariable]] = None, extends: Optional[str] = None, - dependencies: Optional[List[str]] = None, -) -> Dict[str, Any]: + dependencies: Optional[list[str]] = None, +) -> TemplateMetadata: """ Create template metadata dictionary. @@ -231,40 +260,40 @@ def create_template_metadata( # Default variables if none provided if not variables: variables = { - "project_name": { - "description": "Project name (used in documentation and KiCad files)", - "default": name, - }, - "directory_name": { - "description": "Directory/repository name (used for folder structure)", - "default": "%{project_name.lower.replace(' ', '-')}", - }, - "project_filename": { - "description": "Main KiCad project filename (without extension)", - "default": "%{project_name}", - }, + "project_name": TemplateVariable( + description="Project name (used in documentation and KiCad files)", + default=name, + ), + "directory_name": TemplateVariable( + description="Directory/repository name (used for folder structure)", + default="%{project_name.lower.replace(' ', '-')}", + ), + "project_filename": TemplateVariable( + description="Main KiCad project filename (without extension)", + default="%{project_name}", + ), } else: # Make sure we have the predefined variables if not any(k.lower() == "project_name" for k in variables): - variables["project_name"] = { - "description": "Project name (used in documentation and KiCad files)", - "default": name, - } + variables["project_name"] = TemplateVariable( + description="Project name (used in documentation and KiCad files)", + default=name, + ) if not any(k.lower() == "directory_name" for k in variables): - variables["directory_name"] = { - "description": "Directory/repository name (used for folder structure)", - "default": "%{project_name.lower.replace(' ', '-')}", - } + variables["directory_name"] = TemplateVariable( + description="Directory/repository name (used for folder structure)", + default="%{project_name.lower.replace(' ', '-')}", + ) if not any(k.lower() == "project_filename" for k in variables): - variables["project_filename"] = { - "description": "Main KiCad project filename (without extension)", - "default": "%{project_name}", - } + variables["project_filename"] = TemplateVariable( + description="Main KiCad project filename (without extension)", + default="%{project_name}", + ) - metadata = { + metadata: TemplateMetadata = { "name": name, "description": description or f"KiCad project template for {name}", "use_case": use_case or "", @@ -276,13 +305,12 @@ def create_template_metadata( metadata["extends"] = extends if dependencies: - deps_dict: Dict[str, Any] = {"recommended": dependencies} - metadata["dependencies"] = deps_dict + metadata["dependencies"] = TemplateDependencies(recommended=dependencies) return metadata -def write_template_metadata(directory: Path, metadata: Dict[str, Any]) -> None: +def write_template_metadata(directory: Path, metadata: TemplateMetadata) -> None: """ Write template metadata to a file. @@ -300,7 +328,7 @@ def write_template_metadata(directory: Path, metadata: Dict[str, Any]) -> None: def process_markdown_file( - source_file: Path, target_file: Path, variables: Dict[str, Dict[str, Any]] + source_file: Path, target_file: Path, variables: dict[str, TemplateVariable] ) -> None: """ Process a Markdown file to add a template header with available variables. @@ -348,9 +376,9 @@ def process_markdown_file( def create_template_structure( source_directory: Path, template_directory: Path, - metadata: Dict[str, Any], + metadata: TemplateMetadata, gitignore_spec: Optional[pathspec.PathSpec] = None, - additional_excludes: Optional[List[str]] = None, + additional_excludes: Optional[list[str]] = None, ) -> None: """ Create template structure from source directory. @@ -626,8 +654,8 @@ def post_create(context): # readme_path = os.path.join(context["project_dir"], "README.md") # if not os.path.exists(readme_path): # with Path(readme_path).open("w") as f: - # f.write(f"# {context['variables']['project_name']}\\n\\n") - # f.write("Created with KiCad Library Manager\\n") + # f.write(f"# {context['variables']['project_name']}\n\n") + # f.write("Created with KiCad Library Manager\n") # Print message to user click.echo(f"Project {context['variables']['project_name']} created successfully!") @@ -639,7 +667,7 @@ def post_create(context): def process_kicad_project_file( - source_file: Path, target_file: Path, variables: Dict[str, Dict[str, Any]] + source_file: Path, target_file: Path, variables: dict[str, TemplateVariable] ) -> None: """ Process a KiCad project file (.kicad_pro). @@ -782,7 +810,9 @@ def sheet_replacer(match): # Add new functions for rendering templates and creating projects from templates -def render_template_string(template_str: str, variables: Dict[str, Any]) -> str: +def render_template_string( + template_str: str, variables: Mapping[str, Union[str, int, bool]] +) -> str: """ Render a template string using Jinja2 or custom Windows-compatible templating. @@ -808,7 +838,9 @@ def render_template_string(template_str: str, variables: Dict[str, Any]) -> str: return template_str -def render_filename_custom(filename: str, variables: Dict[str, Any]) -> str: +def render_filename_custom( + filename: str, variables: Mapping[str, Union[str, int, bool]] +) -> str: """ Render a filename using a custom Windows-compatible templating system. @@ -827,7 +859,7 @@ def render_filename_custom(filename: str, variables: Dict[str, Any]) -> str: Rendered filename """ - def transform_value(value: str, transformations: List[str]) -> str: + def transform_value(value: str, transformations: list[str]) -> str: """Apply a chain of transformations to a value.""" result = value @@ -902,7 +934,9 @@ def replacer(match): return filename -def render_filename(filename: str, variables: Dict[str, Any]) -> str: +def render_filename( + filename: str, variables: Mapping[str, Union[str, int, bool]] +) -> str: """ Render a filename using either Jinja2 or custom Windows-compatible templating. @@ -932,7 +966,7 @@ def render_filename(filename: str, variables: Dict[str, Any]) -> str: return filename -def find_all_templates(config: Any) -> Dict[str, Dict[str, Any]]: +def find_all_templates(config: Any) -> dict[str, TemplateMetadata]: """ Find all templates in all configured libraries. @@ -943,7 +977,7 @@ def find_all_templates(config: Any) -> Dict[str, Dict[str, Any]]: Dictionary mapping template names to template metadata """ all_templates = {} - all_libraries = config.get_libraries() + all_libraries = config.get_libraries() # type: ignore[attr-defined] for library in all_libraries: library_path = library.get("path") @@ -977,7 +1011,7 @@ def find_all_templates(config: Any) -> Dict[str, Dict[str, Any]]: def render_template_file( source_file: Path, target_file: Path, - variables: Dict[str, Any], + variables: Mapping[str, Union[str, int, bool]], is_binary: bool = False, ) -> bool: """ @@ -1059,8 +1093,8 @@ def render_template_file( def create_project_from_template( template_dir: Path, project_dir: Path, - variables: Dict[str, Any], - metadata: Optional[Dict[str, Any]] = None, + variables: Mapping[str, Union[str, int, bool]], + metadata: Optional[dict[str, Any]] = None, dry_run: bool = False, skip_hooks: bool = False, ) -> bool: @@ -1236,8 +1270,8 @@ def create_project_from_template( def run_post_create_hook( hook_script: Path, project_dir: Path, - variables: Dict[str, Any], - template_metadata: Dict[str, Any], + variables: Mapping[str, Union[str, int, bool]], + template_metadata: dict[str, Any], ) -> None: """ Run a post-creation hook script. diff --git a/memory/constitution.md b/memory/constitution.md new file mode 100644 index 0000000..1ed8d77 --- /dev/null +++ b/memory/constitution.md @@ -0,0 +1,50 @@ +# [PROJECT_NAME] Constitution + + +## Core Principles + +### [PRINCIPLE_1_NAME] + +[PRINCIPLE_1_DESCRIPTION] + + +### [PRINCIPLE_2_NAME] + +[PRINCIPLE_2_DESCRIPTION] + + +### [PRINCIPLE_3_NAME] + +[PRINCIPLE_3_DESCRIPTION] + + +### [PRINCIPLE_4_NAME] + +[PRINCIPLE_4_DESCRIPTION] + + +### [PRINCIPLE_5_NAME] + +[PRINCIPLE_5_DESCRIPTION] + + +## [SECTION_2_NAME] + + +[SECTION_2_CONTENT] + + +## [SECTION_3_NAME] + + +[SECTION_3_CONTENT] + + +## Governance + + +[GOVERNANCE_RULES] + + +**Version**: [CONSTITUTION_VERSION] | **Ratified**: [RATIFICATION_DATE] | **Last Amended**: [LAST_AMENDED_DATE] + \ No newline at end of file diff --git a/memory/constitution_update_checklist.md b/memory/constitution_update_checklist.md new file mode 100644 index 0000000..7f15d7f --- /dev/null +++ b/memory/constitution_update_checklist.md @@ -0,0 +1,85 @@ +# Constitution Update Checklist + +When amending the constitution (`/memory/constitution.md`), ensure all dependent documents are updated to maintain consistency. + +## Templates to Update + +### When adding/modifying ANY article: +- [ ] `/templates/plan-template.md` - Update Constitution Check section +- [ ] `/templates/spec-template.md` - Update if requirements/scope affected +- [ ] `/templates/tasks-template.md` - Update if new task types needed +- [ ] `/.claude/commands/plan.md` - Update if planning process changes +- [ ] `/.claude/commands/tasks.md` - Update if task generation affected +- [ ] `/CLAUDE.md` - Update runtime development guidelines + +### Article-specific updates: + +#### Article I (Library-First): +- [ ] Ensure templates emphasize library creation +- [ ] Update CLI command examples +- [ ] Add llms.txt documentation requirements + +#### Article II (CLI Interface): +- [ ] Update CLI flag requirements in templates +- [ ] Add text I/O protocol reminders + +#### Article III (Test-First): +- [ ] Update test order in all templates +- [ ] Emphasize TDD requirements +- [ ] Add test approval gates + +#### Article IV (Integration Testing): +- [ ] List integration test triggers +- [ ] Update test type priorities +- [ ] Add real dependency requirements + +#### Article V (Observability): +- [ ] Add logging requirements to templates +- [ ] Include multi-tier log streaming +- [ ] Update performance monitoring sections + +#### Article VI (Versioning): +- [ ] Add version increment reminders +- [ ] Include breaking change procedures +- [ ] Update migration requirements + +#### Article VII (Simplicity): +- [ ] Update project count limits +- [ ] Add pattern prohibition examples +- [ ] Include YAGNI reminders + +## Validation Steps + +1. **Before committing constitution changes:** + - [ ] All templates reference new requirements + - [ ] Examples updated to match new rules + - [ ] No contradictions between documents + +2. **After updating templates:** + - [ ] Run through a sample implementation plan + - [ ] Verify all constitution requirements addressed + - [ ] Check that templates are self-contained (readable without constitution) + +3. **Version tracking:** + - [ ] Update constitution version number + - [ ] Note version in template footers + - [ ] Add amendment to constitution history + +## Common Misses + +Watch for these often-forgotten updates: +- Command documentation (`/commands/*.md`) +- Checklist items in templates +- Example code/commands +- Domain-specific variations (web vs mobile vs CLI) +- Cross-references between documents + +## Template Sync Status + +Last sync check: 2025-07-16 +- Constitution version: 2.1.1 +- Templates aligned: ❌ (missing versioning, observability details) + +--- + +*This checklist ensures the constitution's principles are consistently applied across all project documentation.* \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index b98e47a..0cb0fc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "kilm" -version = "0.4.2" +version = "0.5.0" description = "A command-line tool for managing KiCad libraries across projects and workstations" readme = "README.md" license = "MIT" @@ -21,7 +21,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -41,9 +40,11 @@ keywords = [ "pcb-design", "cli", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ "click>=8.0", + "typer>=0.17.4", + "rich>=14.0.0", "packaging>=25.0", "pyyaml>=6.0", "pathlib>=1.0.1", @@ -71,8 +72,7 @@ Repository = "https://github.com/barisgit/KiLM" "Bug Tracker" = "https://github.com/barisgit/KiLM/issues" [project.scripts] -kilm = "kicad_lib_manager.cli:main" -kicad-lib-manager = "kicad_lib_manager.cli:main" +kilm = "kicad_lib_manager.main:app" [tool.hatch.build.targets.wheel] @@ -80,7 +80,7 @@ packages = ["kicad_lib_manager"] [tool.black] line-length = 88 -target-version = ['py38'] +target-version = ['py39'] [tool.pytest.ini_options] testpaths = ["tests"] @@ -97,7 +97,7 @@ addopts = [ ] [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 88 [tool.ruff.lint] @@ -125,7 +125,7 @@ ignore = [ "tests/**/*" = ["ARG", "S101"] [tool.pyrefly] -python-version = "3.8" +python-version = "3.9" [tool.coverage.run] source = ["kicad_lib_manager"] diff --git a/scripts/check-task-prerequisites.sh b/scripts/check-task-prerequisites.sh new file mode 100644 index 0000000..87fca37 --- /dev/null +++ b/scripts/check-task-prerequisites.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Check that implementation plan exists and find optional design documents +# Usage: ./check-task-prerequisites.sh [--json] + +set -e + +JSON_MODE=false +for arg in "$@"; do + case "$arg" in + --json) JSON_MODE=true ;; + --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; + esac +done + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths +eval $(get_feature_paths) + +# Check if on feature branch +check_feature_branch "$CURRENT_BRANCH" || exit 1 + +# Check if feature directory exists +if [[ ! -d "$FEATURE_DIR" ]]; then + echo "ERROR: Feature directory not found: $FEATURE_DIR" + echo "Run /specify first to create the feature structure." + exit 1 +fi + +# Check for implementation plan (required) +if [[ ! -f "$IMPL_PLAN" ]]; then + echo "ERROR: plan.md not found in $FEATURE_DIR" + echo "Run /plan first to create the plan." + exit 1 +fi + +if $JSON_MODE; then + # Build JSON array of available docs that actually exist + docs=() + [[ -f "$RESEARCH" ]] && docs+=("research.md") + [[ -f "$DATA_MODEL" ]] && docs+=("data-model.md") + ([[ -d "$CONTRACTS_DIR" ]] && [[ -n "$(ls -A "$CONTRACTS_DIR" 2>/dev/null)" ]]) && docs+=("contracts/") + [[ -f "$QUICKSTART" ]] && docs+=("quickstart.md") + # join array into JSON + json_docs=$(printf '"%s",' "${docs[@]}") + json_docs="[${json_docs%,}]" + printf '{"FEATURE_DIR":"%s","AVAILABLE_DOCS":%s}\n' "$FEATURE_DIR" "$json_docs" +else + # List available design documents (optional) + echo "FEATURE_DIR:$FEATURE_DIR" + echo "AVAILABLE_DOCS:" + + # Use common check functions + check_file "$RESEARCH" "research.md" + check_file "$DATA_MODEL" "data-model.md" + check_dir "$CONTRACTS_DIR" "contracts/" + check_file "$QUICKSTART" "quickstart.md" +fi + +# Always succeed - task generation should work with whatever docs are available \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 0000000..d636491 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Common functions and variables for all scripts + +# Get repository root +get_repo_root() { + git rev-parse --show-toplevel +} + +# Get current branch +get_current_branch() { + git rev-parse --abbrev-ref HEAD +} + +# Check if current branch is a feature branch +# Returns 0 if valid, 1 if not +check_feature_branch() { + local branch="$1" + if [[ ! "$branch" =~ ^[0-9]{3}- ]]; then + echo "ERROR: Not on a feature branch. Current branch: $branch" + echo "Feature branches should be named like: 001-feature-name" + return 1 + fi + return 0 +} + +# Get feature directory path +get_feature_dir() { + local repo_root="$1" + local branch="$2" + echo "$repo_root/specs/$branch" +} + +# Get all standard paths for a feature +# Usage: eval $(get_feature_paths) +# Sets: REPO_ROOT, CURRENT_BRANCH, FEATURE_DIR, FEATURE_SPEC, IMPL_PLAN, TASKS +get_feature_paths() { + local repo_root=$(get_repo_root) + local current_branch=$(get_current_branch) + local feature_dir=$(get_feature_dir "$repo_root" "$current_branch") + + echo "REPO_ROOT='$repo_root'" + echo "CURRENT_BRANCH='$current_branch'" + echo "FEATURE_DIR='$feature_dir'" + echo "FEATURE_SPEC='$feature_dir/spec.md'" + echo "IMPL_PLAN='$feature_dir/plan.md'" + echo "TASKS='$feature_dir/tasks.md'" + echo "RESEARCH='$feature_dir/research.md'" + echo "DATA_MODEL='$feature_dir/data-model.md'" + echo "QUICKSTART='$feature_dir/quickstart.md'" + echo "CONTRACTS_DIR='$feature_dir/contracts'" +} + +# Check if a file exists and report +check_file() { + local file="$1" + local description="$2" + if [[ -f "$file" ]]; then + echo " ✓ $description" + return 0 + else + echo " ✗ $description" + return 1 + fi +} + +# Check if a directory exists and has files +check_dir() { + local dir="$1" + local description="$2" + if [[ -d "$dir" ]] && [[ -n "$(ls -A "$dir" 2>/dev/null)" ]]; then + echo " ✓ $description" + return 0 + else + echo " ✗ $description" + return 1 + fi +} \ No newline at end of file diff --git a/scripts/create-new-feature.sh b/scripts/create-new-feature.sh new file mode 100644 index 0000000..69ea3c4 --- /dev/null +++ b/scripts/create-new-feature.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Create a new feature with branch, directory structure, and template +# Usage: ./create-new-feature.sh "feature description" +# ./create-new-feature.sh --json "feature description" + +set -e + +JSON_MODE=false + +# Collect non-flag args +ARGS=() +for arg in "$@"; do + case "$arg" in + --json) + JSON_MODE=true + ;; + --help|-h) + echo "Usage: $0 [--json] "; exit 0 ;; + *) + ARGS+=("$arg") ;; + esac +done + +FEATURE_DESCRIPTION="${ARGS[*]}" +if [ -z "$FEATURE_DESCRIPTION" ]; then + echo "Usage: $0 [--json] " >&2 + exit 1 +fi + +# Get repository root +REPO_ROOT=$(git rev-parse --show-toplevel) +SPECS_DIR="$REPO_ROOT/specs" + +# Create specs directory if it doesn't exist +mkdir -p "$SPECS_DIR" + +# Find the highest numbered feature directory +HIGHEST=0 +if [ -d "$SPECS_DIR" ]; then + for dir in "$SPECS_DIR"/*; do + if [ -d "$dir" ]; then + dirname=$(basename "$dir") + number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0") + number=$((10#$number)) + if [ "$number" -gt "$HIGHEST" ]; then + HIGHEST=$number + fi + fi + done +fi + +# Generate next feature number with zero padding +NEXT=$((HIGHEST + 1)) +FEATURE_NUM=$(printf "%03d" "$NEXT") + +# Create branch name from description +BRANCH_NAME=$(echo "$FEATURE_DESCRIPTION" | \ + tr '[:upper:]' '[:lower:]' | \ + sed 's/[^a-z0-9]/-/g' | \ + sed 's/-\+/-/g' | \ + sed 's/^-//' | \ + sed 's/-$//') + +# Extract 2-3 meaningful words +WORDS=$(echo "$BRANCH_NAME" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//') + +# Final branch name +BRANCH_NAME="${FEATURE_NUM}-${WORDS}" + +# Create and switch to new branch +git checkout -b "$BRANCH_NAME" + +# Create feature directory +FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME" +mkdir -p "$FEATURE_DIR" + +# Copy template if it exists +TEMPLATE="$REPO_ROOT/templates/spec-template.md" +SPEC_FILE="$FEATURE_DIR/spec.md" + +if [ -f "$TEMPLATE" ]; then + cp "$TEMPLATE" "$SPEC_FILE" +else + echo "Warning: Template not found at $TEMPLATE" >&2 + touch "$SPEC_FILE" +fi + +if $JSON_MODE; then + printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' \ + "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM" +else + # Output results for the LLM to use (legacy key: value format) + echo "BRANCH_NAME: $BRANCH_NAME" + echo "SPEC_FILE: $SPEC_FILE" + echo "FEATURE_NUM: $FEATURE_NUM" +fi \ No newline at end of file diff --git a/scripts/get-feature-paths.sh b/scripts/get-feature-paths.sh new file mode 100644 index 0000000..bfe5087 --- /dev/null +++ b/scripts/get-feature-paths.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Get paths for current feature branch without creating anything +# Used by commands that need to find existing feature files + +set -e + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths +eval $(get_feature_paths) + +# Check if on feature branch +check_feature_branch "$CURRENT_BRANCH" || exit 1 + +# Output paths (don't create anything) +echo "REPO_ROOT: $REPO_ROOT" +echo "BRANCH: $CURRENT_BRANCH" +echo "FEATURE_DIR: $FEATURE_DIR" +echo "FEATURE_SPEC: $FEATURE_SPEC" +echo "IMPL_PLAN: $IMPL_PLAN" +echo "TASKS: $TASKS" \ No newline at end of file diff --git a/scripts/setup-plan.sh b/scripts/setup-plan.sh new file mode 100644 index 0000000..28bd056 --- /dev/null +++ b/scripts/setup-plan.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Setup implementation plan structure for current branch +# Returns paths needed for implementation plan generation +# Usage: ./setup-plan.sh [--json] + +set -e + +JSON_MODE=false +for arg in "$@"; do + case "$arg" in + --json) JSON_MODE=true ;; + --help|-h) echo "Usage: $0 [--json]"; exit 0 ;; + esac +done + +# Source common functions +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/common.sh" + +# Get all paths +eval $(get_feature_paths) + +# Check if on feature branch +check_feature_branch "$CURRENT_BRANCH" || exit 1 + +# Create specs directory if it doesn't exist +mkdir -p "$FEATURE_DIR" + +# Copy plan template if it exists +TEMPLATE="$REPO_ROOT/templates/plan-template.md" +if [ -f "$TEMPLATE" ]; then + cp "$TEMPLATE" "$IMPL_PLAN" +fi + +if $JSON_MODE; then + printf '{"FEATURE_SPEC":"%s","IMPL_PLAN":"%s","SPECS_DIR":"%s","BRANCH":"%s"}\n' \ + "$FEATURE_SPEC" "$IMPL_PLAN" "$FEATURE_DIR" "$CURRENT_BRANCH" +else + # Output all paths for LLM use + echo "FEATURE_SPEC: $FEATURE_SPEC" + echo "IMPL_PLAN: $IMPL_PLAN" + echo "SPECS_DIR: $FEATURE_DIR" + echo "BRANCH: $CURRENT_BRANCH" +fi \ No newline at end of file diff --git a/scripts/update-agent-context.sh b/scripts/update-agent-context.sh new file mode 100644 index 0000000..51fa640 --- /dev/null +++ b/scripts/update-agent-context.sh @@ -0,0 +1,234 @@ +#!/bin/bash +# Incrementally update agent context files based on new feature plan +# Supports: CLAUDE.md, GEMINI.md, and .github/copilot-instructions.md +# O(1) operation - only reads current context file and new plan.md + +set -e + +REPO_ROOT=$(git rev-parse --show-toplevel) +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) +FEATURE_DIR="$REPO_ROOT/specs/$CURRENT_BRANCH" +NEW_PLAN="$FEATURE_DIR/plan.md" + +# Determine which agent context files to update +CLAUDE_FILE="$REPO_ROOT/CLAUDE.md" +GEMINI_FILE="$REPO_ROOT/GEMINI.md" +COPILOT_FILE="$REPO_ROOT/.github/copilot-instructions.md" + +# Allow override via argument +AGENT_TYPE="$1" + +if [ ! -f "$NEW_PLAN" ]; then + echo "ERROR: No plan.md found at $NEW_PLAN" + exit 1 +fi + +echo "=== Updating agent context files for feature $CURRENT_BRANCH ===" + +# Extract tech from new plan +NEW_LANG=$(grep "^**Language/Version**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Language\/Version**: //' | grep -v "NEEDS CLARIFICATION" || echo "") +NEW_FRAMEWORK=$(grep "^**Primary Dependencies**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Primary Dependencies**: //' | grep -v "NEEDS CLARIFICATION" || echo "") +NEW_TESTING=$(grep "^**Testing**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Testing**: //' | grep -v "NEEDS CLARIFICATION" || echo "") +NEW_DB=$(grep "^**Storage**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Storage**: //' | grep -v "N/A" | grep -v "NEEDS CLARIFICATION" || echo "") +NEW_PROJECT_TYPE=$(grep "^**Project Type**: " "$NEW_PLAN" 2>/dev/null | head -1 | sed 's/^**Project Type**: //' || echo "") + +# Function to update a single agent context file +update_agent_file() { + local target_file="$1" + local agent_name="$2" + + echo "Updating $agent_name context file: $target_file" + + # Create temp file for new context + local temp_file=$(mktemp) + + # If file doesn't exist, create from template + if [ ! -f "$target_file" ]; then + echo "Creating new $agent_name context file..." + + # Check if this is the SDD repo itself + if [ -f "$REPO_ROOT/templates/agent-file-template.md" ]; then + cp "$REPO_ROOT/templates/agent-file-template.md" "$temp_file" + else + echo "ERROR: Template not found at $REPO_ROOT/templates/agent-file-template.md" + return 1 + fi + + # Replace placeholders + sed -i.bak "s/\[PROJECT NAME\]/$(basename $REPO_ROOT)/" "$temp_file" + sed -i.bak "s/\[DATE\]/$(date +%Y-%m-%d)/" "$temp_file" + sed -i.bak "s/\[EXTRACTED FROM ALL PLAN.MD FILES\]/- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)/" "$temp_file" + + # Add project structure based on type + if [[ "$NEW_PROJECT_TYPE" == *"web"* ]]; then + sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|backend/\nfrontend/\ntests/|" "$temp_file" + else + sed -i.bak "s|\[ACTUAL STRUCTURE FROM PLANS\]|src/\ntests/|" "$temp_file" + fi + + # Add minimal commands + if [[ "$NEW_LANG" == *"Python"* ]]; then + COMMANDS="cd src && pytest && ruff check ." + elif [[ "$NEW_LANG" == *"Rust"* ]]; then + COMMANDS="cargo test && cargo clippy" + elif [[ "$NEW_LANG" == *"JavaScript"* ]] || [[ "$NEW_LANG" == *"TypeScript"* ]]; then + COMMANDS="npm test && npm run lint" + else + COMMANDS="# Add commands for $NEW_LANG" + fi + sed -i.bak "s|\[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES\]|$COMMANDS|" "$temp_file" + + # Add code style + sed -i.bak "s|\[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE\]|$NEW_LANG: Follow standard conventions|" "$temp_file" + + # Add recent changes + sed -i.bak "s|\[LAST 3 FEATURES AND WHAT THEY ADDED\]|- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK|" "$temp_file" + + rm "$temp_file.bak" + else + echo "Updating existing $agent_name context file..." + + # Extract manual additions + local manual_start=$(grep -n "" "$target_file" | cut -d: -f1) + local manual_end=$(grep -n "" "$target_file" | cut -d: -f1) + + if [ ! -z "$manual_start" ] && [ ! -z "$manual_end" ]; then + sed -n "${manual_start},${manual_end}p" "$target_file" > /tmp/manual_additions.txt + fi + + # Parse existing file and create updated version + python3 - << EOF +import re +import sys +from datetime import datetime + +# Read existing file +with open("$target_file", 'r') as f: + content = f.read() + +# Check if new tech already exists +tech_section = re.search(r'## Active Technologies\n(.*?)\n\n', content, re.DOTALL) +if tech_section: + existing_tech = tech_section.group(1) + + # Add new tech if not already present + new_additions = [] + if "$NEW_LANG" and "$NEW_LANG" not in existing_tech: + new_additions.append(f"- $NEW_LANG + $NEW_FRAMEWORK ($CURRENT_BRANCH)") + if "$NEW_DB" and "$NEW_DB" not in existing_tech and "$NEW_DB" != "N/A": + new_additions.append(f"- $NEW_DB ($CURRENT_BRANCH)") + + if new_additions: + updated_tech = existing_tech + "\n" + "\n".join(new_additions) + content = content.replace(tech_section.group(0), f"## Active Technologies\n{updated_tech}\n\n") + +# Update project structure if needed +if "$NEW_PROJECT_TYPE" == "web" and "frontend/" not in content: + struct_section = re.search(r'## Project Structure\n\`\`\`\n(.*?)\n\`\`\`', content, re.DOTALL) + if struct_section: + updated_struct = struct_section.group(1) + "\nfrontend/src/ # Web UI" + content = re.sub(r'(## Project Structure\n\`\`\`\n).*?(\n\`\`\`)', + f'\\1{updated_struct}\\2', content, flags=re.DOTALL) + +# Add new commands if language is new +if "$NEW_LANG" and f"# {NEW_LANG}" not in content: + commands_section = re.search(r'## Commands\n\`\`\`bash\n(.*?)\n\`\`\`', content, re.DOTALL) + if not commands_section: + commands_section = re.search(r'## Commands\n(.*?)\n\n', content, re.DOTALL) + + if commands_section: + new_commands = commands_section.group(1) + if "Python" in "$NEW_LANG": + new_commands += "\ncd src && pytest && ruff check ." + elif "Rust" in "$NEW_LANG": + new_commands += "\ncargo test && cargo clippy" + elif "JavaScript" in "$NEW_LANG" or "TypeScript" in "$NEW_LANG": + new_commands += "\nnpm test && npm run lint" + + if "```bash" in content: + content = re.sub(r'(## Commands\n\`\`\`bash\n).*?(\n\`\`\`)', + f'\\1{new_commands}\\2', content, flags=re.DOTALL) + else: + content = re.sub(r'(## Commands\n).*?(\n\n)', + f'\\1{new_commands}\\2', content, flags=re.DOTALL) + +# Update recent changes (keep only last 3) +changes_section = re.search(r'## Recent Changes\n(.*?)(\n\n|$)', content, re.DOTALL) +if changes_section: + changes = changes_section.group(1).strip().split('\n') + changes.insert(0, f"- $CURRENT_BRANCH: Added $NEW_LANG + $NEW_FRAMEWORK") + # Keep only last 3 + changes = changes[:3] + content = re.sub(r'(## Recent Changes\n).*?(\n\n|$)', + f'\\1{chr(10).join(changes)}\\2', content, flags=re.DOTALL) + +# Update date +content = re.sub(r'Last updated: \d{4}-\d{2}-\d{2}', + f'Last updated: {datetime.now().strftime("%Y-%m-%d")}', content) + +# Write to temp file +with open("$temp_file", 'w') as f: + f.write(content) +EOF + + # Restore manual additions if they exist + if [ -f /tmp/manual_additions.txt ]; then + # Remove old manual section from temp file + sed -i.bak '//,//d' "$temp_file" + # Append manual additions + cat /tmp/manual_additions.txt >> "$temp_file" + rm /tmp/manual_additions.txt "$temp_file.bak" + fi + fi + + # Move temp file to final location + mv "$temp_file" "$target_file" + echo "✅ $agent_name context file updated successfully" +} + +# Update files based on argument or detect existing files +case "$AGENT_TYPE" in + "claude") + update_agent_file "$CLAUDE_FILE" "Claude Code" + ;; + "gemini") + update_agent_file "$GEMINI_FILE" "Gemini CLI" + ;; + "copilot") + update_agent_file "$COPILOT_FILE" "GitHub Copilot" + ;; + "") + # Update all existing files + [ -f "$CLAUDE_FILE" ] && update_agent_file "$CLAUDE_FILE" "Claude Code" + [ -f "$GEMINI_FILE" ] && update_agent_file "$GEMINI_FILE" "Gemini CLI" + [ -f "$COPILOT_FILE" ] && update_agent_file "$COPILOT_FILE" "GitHub Copilot" + + # If no files exist, create based on current directory or ask user + if [ ! -f "$CLAUDE_FILE" ] && [ ! -f "$GEMINI_FILE" ] && [ ! -f "$COPILOT_FILE" ]; then + echo "No agent context files found. Creating Claude Code context file by default." + update_agent_file "$CLAUDE_FILE" "Claude Code" + fi + ;; + *) + echo "ERROR: Unknown agent type '$AGENT_TYPE'. Use: claude, gemini, copilot, or leave empty for all." + exit 1 + ;; +esac +echo "" +echo "Summary of changes:" +if [ ! -z "$NEW_LANG" ]; then + echo "- Added language: $NEW_LANG" +fi +if [ ! -z "$NEW_FRAMEWORK" ]; then + echo "- Added framework: $NEW_FRAMEWORK" +fi +if [ ! -z "$NEW_DB" ] && [ "$NEW_DB" != "N/A" ]; then + echo "- Added database: $NEW_DB" +fi + +echo "" +echo "Usage: $0 [claude|gemini|copilot]" +echo " - No argument: Update all existing agent context files" +echo " - claude: Update only CLAUDE.md" +echo " - gemini: Update only GEMINI.md" +echo " - copilot: Update only .github/copilot-instructions.md" \ No newline at end of file diff --git a/tests/test_config_commands.py b/tests/test_config_commands.py index 7a93100..f76d34d 100644 --- a/tests/test_config_commands.py +++ b/tests/test_config_commands.py @@ -2,16 +2,14 @@ Tests for KiCad Library Manager config commands. """ -from typing import List - import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from kicad_lib_manager.cli import main -from kicad_lib_manager.config import Config, LibraryDict +from kicad_lib_manager.main import app as main +from kicad_lib_manager.services.config_service import Config, LibraryDict # Sample test data -TEST_LIBRARIES: List[LibraryDict] = [ +TEST_LIBRARIES: list[LibraryDict] = [ LibraryDict(name="test-github-lib", path="/path/to/github/library", type="github"), LibraryDict(name="test-cloud-lib", path="/path/to/cloud/library", type="cloud"), ] @@ -55,12 +53,24 @@ def test_config_list(mock_config_class): result = runner.invoke(main, ["config", "list"]) assert result.exit_code == 0 - assert "Configured Libraries" in result.output assert "GitHub Libraries" in result.output assert "Cloud Libraries" in result.output assert "test-github-lib" in result.output assert "test-cloud-lib" in result.output - assert "(current)" in result.output # Current library should be marked + assert "✓ Current" in result.output # Current library should be marked + + +def test_config_list_verbose(mock_config_class): + """Test the 'kilm config list --verbose' command.""" + runner = CliRunner() + result = runner.invoke(main, ["config", "list", "--verbose"]) + + assert result.exit_code == 0 + assert "GitHub Libraries" in result.output + assert "Cloud Libraries" in result.output + assert "test-github-lib" in result.output + assert "test-cloud-lib" in result.output + assert "✓ CURRENT" in result.output def test_config_list_filtered(mock_config_class): diff --git a/tests/test_library_manager.py b/tests/test_library_manager.py index cb77c58..e4a08b1 100644 --- a/tests/test_library_manager.py +++ b/tests/test_library_manager.py @@ -3,32 +3,28 @@ import pytest -from kicad_lib_manager.library_manager import ( - add_entries_to_table, - add_libraries, - format_uri, -) +from kicad_lib_manager.services.library_service import LibraryService def test_format_uri_absolute_path(): """Test URI formatting with absolute paths.""" # Test Unix-style paths assert ( - format_uri("/path/to/lib", "test_lib", "symbols") + LibraryService.format_uri("/path/to/lib", "test_lib", "symbols") == "/path/to/lib/symbols/test_lib.kicad_sym" ) assert ( - format_uri("/path/to/lib", "test_lib", "footprints") + LibraryService.format_uri("/path/to/lib", "test_lib", "footprints") == "/path/to/lib/footprints/test_lib.pretty" ) # Test Windows-style paths assert ( - format_uri("C:\\path\\to\\lib", "test_lib", "symbols") + LibraryService.format_uri("C:\\path\\to\\lib", "test_lib", "symbols") == "C:/path/to/lib/symbols/test_lib.kicad_sym" ) assert ( - format_uri("C:\\path\\to\\lib", "test_lib", "footprints") + LibraryService.format_uri("C:\\path\\to\\lib", "test_lib", "footprints") == "C:/path/to/lib/footprints/test_lib.pretty" ) @@ -36,11 +32,11 @@ def test_format_uri_absolute_path(): def test_format_uri_env_var(): """Test URI formatting with environment variable names.""" assert ( - format_uri("KICAD_LIB", "test_lib", "symbols") + LibraryService.format_uri("KICAD_LIB", "test_lib", "symbols") == "${KICAD_LIB}/symbols/test_lib.kicad_sym" ) assert ( - format_uri("KICAD_LIB", "test_lib", "footprints") + LibraryService.format_uri("KICAD_LIB", "test_lib", "footprints") == "${KICAD_LIB}/footprints/test_lib.pretty" ) @@ -49,13 +45,13 @@ def test_format_uri_path_in_curly(): """Test URI formatting with paths already in ${} format.""" # Test absolute paths in ${} assert ( - format_uri("${/path/to/lib}", "test_lib", "symbols") + LibraryService.format_uri("${/path/to/lib}", "test_lib", "symbols") == "/path/to/lib/symbols/test_lib.kicad_sym" ) # Test environment variables in ${} assert ( - format_uri("${KICAD_LIB}", "test_lib", "symbols") + LibraryService.format_uri("${KICAD_LIB}", "test_lib", "symbols") == "${KICAD_LIB}/symbols/test_lib.kicad_sym" ) @@ -64,30 +60,31 @@ def test_format_uri_edge_cases(): """Test URI formatting with edge cases.""" # Test with empty library name assert ( - format_uri("/path/to/lib", "", "symbols") == "/path/to/lib/symbols/.kicad_sym" + LibraryService.format_uri("/path/to/lib", "", "symbols") + == "/path/to/lib/symbols/.kicad_sym" ) # Test with special characters in library name assert ( - format_uri("/path/to/lib", "test-lib_123", "symbols") + LibraryService.format_uri("/path/to/lib", "test-lib_123", "symbols") == "/path/to/lib/symbols/test-lib_123.kicad_sym" ) # Test with mixed slashes assert ( - format_uri("C:/path\\to/lib", "test_lib", "symbols") + LibraryService.format_uri("C:/path\\to/lib", "test_lib", "symbols") == "C:/path/to/lib/symbols/test_lib.kicad_sym" ) # Test with UTF-8 characters in path assert ( - format_uri("/path/to/šžć", "test_lib", "symbols") + LibraryService.format_uri("/path/to/šžć", "test_lib", "symbols") == "/path/to/šžć/symbols/test_lib.kicad_sym" ) # Test with UTF-8 characters and spaces in path assert ( - format_uri("/path/to /šžć ", "test_lib", "symbols") + LibraryService.format_uri("/path/to /šžć ", "test_lib", "symbols") == "/path/to /šžć /symbols/test_lib.kicad_sym" ) @@ -95,13 +92,15 @@ def test_format_uri_edge_cases(): def test_format_uri_invalid_input(): """Test URI formatting with invalid inputs.""" with pytest.raises(ValueError): - format_uri("", "test_lib", "symbols") # Empty base path + LibraryService.format_uri("", "test_lib", "symbols") # Empty base path with pytest.raises(ValueError): - format_uri("/path/to/lib", "test_lib", "invalid_type") # Invalid library type + LibraryService.format_uri( + "/path/to/lib", "test_lib", "invalid_type" + ) # Invalid library type with pytest.raises(ValueError): - format_uri("${unclosed", "test_lib", "symbols") # Unclosed ${ + LibraryService.format_uri("${unclosed", "test_lib", "symbols") # Unclosed ${ def test_add_libraries_integration(tmp_path): @@ -118,20 +117,23 @@ def test_add_libraries_integration(tmp_path): (lib_dir / "symbols" / "test_lib.kicad_sym").touch() (lib_dir / "footprints" / "test_lib.pretty").touch() + # Create LibraryService instance + service = LibraryService() + # Test with absolute path - added_libs, changes = add_libraries(str(lib_dir), config_dir, dry_run=True) + added_libs, changes = service.add_libraries(str(lib_dir), config_dir, dry_run=True) assert "test_lib" in added_libs assert changes # Test with environment variable path - we need to set up the environment variable first os.environ["KICAD_LIB"] = str(lib_dir) - added_libs, changes = add_libraries("KICAD_LIB", config_dir, dry_run=True) + added_libs, changes = service.add_libraries("KICAD_LIB", config_dir, dry_run=True) assert "test_lib" in added_libs assert changes # Test with path in ${} - use a proper environment variable name os.environ["TEST_LIB"] = str(lib_dir) - added_libs, changes = add_libraries("${TEST_LIB}", config_dir, dry_run=True) + added_libs, changes = service.add_libraries("${TEST_LIB}", config_dir, dry_run=True) assert "test_lib" in added_libs assert changes @@ -152,8 +154,11 @@ def test_add_libraries_utf8(tmp_path): lib_dir / "footprints" / "test_šž.pretty" ).mkdir() # Footprint libs are directories + # Create LibraryService instance + service = LibraryService() + # Test adding libraries with UTF-8 paths/names - added_libs, changes = add_libraries(str(lib_dir), config_dir, dry_run=True) + added_libs, changes = service.add_libraries(str(lib_dir), config_dir, dry_run=True) # Assert that the libraries with UTF-8 names were detected assert "test_čš" in added_libs @@ -185,7 +190,7 @@ def test_add_entries_with_special_chars(tmp_path): ] # Add entries - add_entries_to_table(table_path, entries) + LibraryService.add_entries_to_table(table_path, entries) # Read the updated table with Path(table_path).open(encoding="utf-8") as f: diff --git a/tests/test_sync_command.py b/tests/test_sync_command.py index 2cbaab8..1fd952e 100644 --- a/tests/test_sync_command.py +++ b/tests/test_sync_command.py @@ -3,13 +3,13 @@ """ from pathlib import Path -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, patch import pytest -from click.testing import CliRunner +from typer.testing import CliRunner -from kicad_lib_manager.cli import main from kicad_lib_manager.commands.sync.command import check_for_library_changes +from kicad_lib_manager.main import app as main # Sample test libraries TEST_LIBRARIES = [ @@ -121,26 +121,18 @@ def test_sync_command(mock_config, mock_subprocess_run, mock_path_methods): def test_sync_with_auto_setup(mock_config, mock_subprocess_run, mock_path_methods): """Test sync with auto-setup option.""" - # Mock the setup module import - setup_module_mock = Mock() - setup_command_mock = Mock(name="setup_command") - setup_command_mock.make_context = Mock(return_value=Mock()) - setup_command_mock.invoke = Mock() - setup_module_mock.setup = setup_command_mock - - # Mock the module import - with patch.dict( - "sys.modules", {"kicad_lib_manager.commands.setup": setup_module_mock} - ), patch("click.get_current_context", return_value=Mock()): + # Mock the setup command function (imported within sync function) + with patch("kicad_lib_manager.commands.setup.command.setup") as mock_setup: + mock_setup.return_value = None # setup function returns None on success + runner = CliRunner() result = runner.invoke(main, ["sync", "--auto-setup"]) assert result.exit_code == 0 assert "Running 'kilm setup'" in result.output - # Verify that make_context and invoke were called - setup_command_mock.make_context.assert_called_once() - setup_command_mock.invoke.assert_called_once() + # Verify that setup was called + mock_setup.assert_called_once() def test_sync_with_already_up_to_date(mock_config, mock_path_methods): @@ -245,9 +237,11 @@ def test_check_for_library_changes_fallback(): mock_run.return_value = result # Mock file existence with a patch for fallback behavior - with patch.object(Path, "exists", return_value=True), patch.object( - Path, "is_dir", return_value=True - ), patch.object(Path, "glob") as mock_glob: + with ( + patch.object(Path, "exists", return_value=True), + patch.object(Path, "is_dir", return_value=True), + patch.object(Path, "glob") as mock_glob, + ): # Setup mock glob to return symbol files def mock_glob_func(pattern): if "**/*.kicad_sym" in pattern: diff --git a/tests/test_unpin_command.py b/tests/test_unpin_command.py index bbf8d95..228d527 100644 --- a/tests/test_unpin_command.py +++ b/tests/test_unpin_command.py @@ -5,9 +5,9 @@ from pathlib import Path from unittest.mock import patch -from click.testing import CliRunner +from typer.testing import CliRunner -from kicad_lib_manager.commands.unpin import unpin +from kicad_lib_manager.main import app as main class TestUnpinCommand: @@ -19,16 +19,16 @@ def setup_method(self): def test_mutual_exclusivity_all_with_symbols(self): """Test that --all cannot be used with --symbols.""" - result = self.runner.invoke(unpin, ["--all", "--symbols", "lib1"]) - assert result.exit_code == 2 # Click usage error exit code + result = self.runner.invoke(main, ["unpin", "--all", "--symbols", "lib1"]) + assert result.exit_code == 1 # Typer validation error exit code assert ( "'--all' cannot be used with '--symbols' or '--footprints'" in result.output ) def test_mutual_exclusivity_all_with_footprints(self): """Test that --all cannot be used with --footprints.""" - result = self.runner.invoke(unpin, ["--all", "--footprints", "lib1"]) - assert result.exit_code == 2 # Click usage error exit code + result = self.runner.invoke(main, ["unpin", "--all", "--footprints", "lib1"]) + assert result.exit_code == 1 # Typer validation error exit code assert ( "'--all' cannot be used with '--symbols' or '--footprints'" in result.output ) @@ -36,9 +36,9 @@ def test_mutual_exclusivity_all_with_footprints(self): def test_mutual_exclusivity_all_with_both(self): """Test that --all cannot be used with both --symbols and --footprints.""" result = self.runner.invoke( - unpin, ["--all", "--symbols", "lib1", "--footprints", "lib2"] + main, ["unpin", "--all", "--symbols", "lib1", "--footprints", "lib2"] ) - assert result.exit_code == 2 # Click usage error exit code + assert result.exit_code == 1 # Typer validation error exit code assert ( "'--all' cannot be used with '--symbols' or '--footprints'" in result.output ) @@ -46,7 +46,7 @@ def test_mutual_exclusivity_all_with_both(self): def test_mutual_exclusivity_all_only(self): """Test that --all can be used without --symbols or --footprints.""" with patch( - "kicad_lib_manager.commands.unpin.command.find_kicad_config" + "kicad_lib_manager.services.library_service.LibraryService.find_kicad_config" ) as mock_find_config: mock_find_config.return_value = Path("/tmp/kicad") @@ -59,14 +59,14 @@ def test_mutual_exclusivity_all_only(self): ) # Should not raise an error - result = self.runner.invoke(unpin, ["--all"]) + result = self.runner.invoke(main, ["unpin", "--all"]) # Exit code 0 means success, or 1 if no libraries found (which is expected) assert result.exit_code in [0, 1] def test_mutual_exclusivity_symbols_only(self): """Test that --symbols can be used without --all.""" with patch( - "kicad_lib_manager.commands.unpin.command.find_kicad_config" + "kicad_lib_manager.services.library_service.LibraryService.find_kicad_config" ) as mock_find_config: mock_find_config.return_value = Path("/tmp/kicad") @@ -77,14 +77,14 @@ def test_mutual_exclusivity_symbols_only(self): mock_open.return_value.__enter__.return_value.read.return_value = '{"session": {"pinned_symbol_libs": ["lib1"], "pinned_fp_libs": []}}' # Should not raise an error - result = self.runner.invoke(unpin, ["--symbols", "lib1"]) + result = self.runner.invoke(main, ["unpin", "--symbols", "lib1"]) # Exit code 0 means success, or 1 if no libraries found (which is expected) assert result.exit_code in [0, 1] def test_mutual_exclusivity_footprints_only(self): """Test that --footprints can be used without --all.""" with patch( - "kicad_lib_manager.commands.unpin.command.find_kicad_config" + "kicad_lib_manager.services.library_service.LibraryService.find_kicad_config" ) as mock_find_config: mock_find_config.return_value = Path("/tmp/kicad") @@ -95,6 +95,6 @@ def test_mutual_exclusivity_footprints_only(self): mock_open.return_value.__enter__.return_value.read.return_value = '{"session": {"pinned_symbol_libs": [], "pinned_fp_libs": ["lib1"]}}' # Should not raise an error - result = self.runner.invoke(unpin, ["--footprints", "lib1"]) + result = self.runner.invoke(main, ["unpin", "--footprints", "lib1"]) # Exit code 0 means success, or 1 if no libraries found (which is expected) assert result.exit_code in [0, 1] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..1347708 --- /dev/null +++ b/uv.lock @@ -0,0 +1,920 @@ +version = 1 +revision = 3 +requires-python = ">=3.9" +resolution-markers = [ + "python_full_version >= '3.10'", + "python_full_version < '3.10'", +] + +[[package]] +name = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "certifi" +version = "2025.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/98/f3b8013223728a99b908c9344da3aa04ee6e3fa235f19409033eda92fb78/charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72", size = 207695, upload-time = "2025-08-09T07:55:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/21/40/5188be1e3118c82dcb7c2a5ba101b783822cfb413a0268ed3be0468532de/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe", size = 147153, upload-time = "2025-08-09T07:55:38.467Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/5d0d74bc1e1380f0b72c327948d9c2aca14b46a9efd87604e724260f384c/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601", size = 160428, upload-time = "2025-08-09T07:55:40.072Z" }, + { url = "https://files.pythonhosted.org/packages/85/9a/d891f63722d9158688de58d050c59dc3da560ea7f04f4c53e769de5140f5/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c", size = 157627, upload-time = "2025-08-09T07:55:41.706Z" }, + { url = "https://files.pythonhosted.org/packages/65/1a/7425c952944a6521a9cfa7e675343f83fd82085b8af2b1373a2409c683dc/charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2", size = 152388, upload-time = "2025-08-09T07:55:43.262Z" }, + { url = "https://files.pythonhosted.org/packages/f0/c9/a2c9c2a355a8594ce2446085e2ec97fd44d323c684ff32042e2a6b718e1d/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0", size = 150077, upload-time = "2025-08-09T07:55:44.903Z" }, + { url = "https://files.pythonhosted.org/packages/3b/38/20a1f44e4851aa1c9105d6e7110c9d020e093dfa5836d712a5f074a12bf7/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0", size = 161631, upload-time = "2025-08-09T07:55:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/a4/fa/384d2c0f57edad03d7bec3ebefb462090d8905b4ff5a2d2525f3bb711fac/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0", size = 159210, upload-time = "2025-08-09T07:55:47.539Z" }, + { url = "https://files.pythonhosted.org/packages/33/9e/eca49d35867ca2db336b6ca27617deed4653b97ebf45dfc21311ce473c37/charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a", size = 153739, upload-time = "2025-08-09T07:55:48.744Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/26c3036e62dfe8de8061182d33be5025e2424002125c9500faff74a6735e/charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f", size = 99825, upload-time = "2025-08-09T07:55:50.305Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/f05db471f81af1fa01839d44ae2a8bfeec8d2a8b4590f16c4e7393afd323/charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669", size = 107452, upload-time = "2025-08-09T07:55:51.461Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/65/ca/2135ac97709b400c7654b4b764daf5c5567c2da45a30cdd20f9eefe2d658/charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe", size = 205326, upload-time = "2025-08-09T07:56:24.721Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/98a04c3c97dd34e49c7d247083af03645ca3730809a5509443f3c37f7c99/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8", size = 146008, upload-time = "2025-08-09T07:56:26.004Z" }, + { url = "https://files.pythonhosted.org/packages/60/f5/4659a4cb3c4ec146bec80c32d8bb16033752574c20b1252ee842a95d1a1e/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9", size = 159196, upload-time = "2025-08-09T07:56:27.25Z" }, + { url = "https://files.pythonhosted.org/packages/86/9e/f552f7a00611f168b9a5865a1414179b2c6de8235a4fa40189f6f79a1753/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31", size = 156819, upload-time = "2025-08-09T07:56:28.515Z" }, + { url = "https://files.pythonhosted.org/packages/7e/95/42aa2156235cbc8fa61208aded06ef46111c4d3f0de233107b3f38631803/charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f", size = 151350, upload-time = "2025-08-09T07:56:29.716Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a9/3865b02c56f300a6f94fc631ef54f0a8a29da74fb45a773dfd3dcd380af7/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927", size = 148644, upload-time = "2025-08-09T07:56:30.984Z" }, + { url = "https://files.pythonhosted.org/packages/77/d9/cbcf1a2a5c7d7856f11e7ac2d782aec12bdfea60d104e60e0aa1c97849dc/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9", size = 160468, upload-time = "2025-08-09T07:56:32.252Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/6f45efee8697b89fda4d50580f292b8f7f9306cb2971d4b53f8914e4d890/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5", size = 158187, upload-time = "2025-08-09T07:56:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/70/99/f1c3bdcfaa9c45b3ce96f70b14f070411366fa19549c1d4832c935d8e2c3/charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc", size = 152699, upload-time = "2025-08-09T07:56:34.739Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ad/b0081f2f99a4b194bcbb1934ef3b12aa4d9702ced80a37026b7607c72e58/charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce", size = 99580, upload-time = "2025-08-09T07:56:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/9a/8f/ae790790c7b64f925e5c953b924aaa42a243fb778fed9e41f147b2a5715a/charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef", size = 107366, upload-time = "2025-08-09T07:56:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/b5a06ad970ddc7a0e513112d40113e834638f4ca1120eb727a249fb2715e/charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15", size = 204342, upload-time = "2025-08-09T07:56:38.687Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ec/1edc30a377f0a02689342f214455c3f6c2fbedd896a1d2f856c002fc3062/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db", size = 145995, upload-time = "2025-08-09T07:56:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/5e67ab85e6d22b04641acb5399c8684f4d37caf7558a53859f0283a650e9/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d", size = 158640, upload-time = "2025-08-09T07:56:41.311Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e5/38421987f6c697ee3722981289d554957c4be652f963d71c5e46a262e135/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096", size = 156636, upload-time = "2025-08-09T07:56:43.195Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e4/5a075de8daa3ec0745a9a3b54467e0c2967daaaf2cec04c845f73493e9a1/charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa", size = 150939, upload-time = "2025-08-09T07:56:44.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/f7/3611b32318b30974131db62b4043f335861d4d9b49adc6d57c1149cc49d4/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049", size = 148580, upload-time = "2025-08-09T07:56:46.684Z" }, + { url = "https://files.pythonhosted.org/packages/7e/61/19b36f4bd67f2793ab6a99b979b4e4f3d8fc754cbdffb805335df4337126/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0", size = 159870, upload-time = "2025-08-09T07:56:47.941Z" }, + { url = "https://files.pythonhosted.org/packages/06/57/84722eefdd338c04cf3030ada66889298eaedf3e7a30a624201e0cbe424a/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92", size = 157797, upload-time = "2025-08-09T07:56:49.756Z" }, + { url = "https://files.pythonhosted.org/packages/72/2a/aff5dd112b2f14bcc3462c312dce5445806bfc8ab3a7328555da95330e4b/charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16", size = 152224, upload-time = "2025-08-09T07:56:51.369Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8c/9839225320046ed279c6e839d51f028342eb77c91c89b8ef2549f951f3ec/charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce", size = 100086, upload-time = "2025-08-09T07:56:52.722Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7a/36fbcf646e41f710ce0a563c1c9a343c6edf9be80786edeb15b6f62e17db/charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c", size = 107400, upload-time = "2025-08-09T07:56:55.172Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ca/9a0983dd5c8e9733565cf3db4df2b0a2e9a82659fd8aa2a868ac6e4a991f/charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05", size = 207520, upload-time = "2025-08-09T07:57:11.026Z" }, + { url = "https://files.pythonhosted.org/packages/39/c6/99271dc37243a4f925b09090493fb96c9333d7992c6187f5cfe5312008d2/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e", size = 147307, upload-time = "2025-08-09T07:57:12.4Z" }, + { url = "https://files.pythonhosted.org/packages/e4/69/132eab043356bba06eb333cc2cc60c6340857d0a2e4ca6dc2b51312886b3/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99", size = 160448, upload-time = "2025-08-09T07:57:13.712Z" }, + { url = "https://files.pythonhosted.org/packages/04/9a/914d294daa4809c57667b77470533e65def9c0be1ef8b4c1183a99170e9d/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7", size = 157758, upload-time = "2025-08-09T07:57:14.979Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a8/6f5bcf1bcf63cb45625f7c5cadca026121ff8a6c8a3256d8d8cd59302663/charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7", size = 152487, upload-time = "2025-08-09T07:57:16.332Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/d3d0e9592f4e504f9dea08b8db270821c909558c353dc3b457ed2509f2fb/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19", size = 150054, upload-time = "2025-08-09T07:57:17.576Z" }, + { url = "https://files.pythonhosted.org/packages/20/30/5f64fe3981677fe63fa987b80e6c01042eb5ff653ff7cec1b7bd9268e54e/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312", size = 161703, upload-time = "2025-08-09T07:57:20.012Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ef/dd08b2cac9284fd59e70f7d97382c33a3d0a926e45b15fc21b3308324ffd/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc", size = 159096, upload-time = "2025-08-09T07:57:21.329Z" }, + { url = "https://files.pythonhosted.org/packages/45/8c/dcef87cfc2b3f002a6478f38906f9040302c68aebe21468090e39cde1445/charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34", size = 153852, upload-time = "2025-08-09T07:57:22.608Z" }, + { url = "https://files.pythonhosted.org/packages/63/86/9cbd533bd37883d467fcd1bd491b3547a3532d0fbb46de2b99feeebf185e/charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432", size = 99840, upload-time = "2025-08-09T07:57:23.883Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d6/7e805c8e5c46ff9729c49950acc4ee0aeb55efb8b3a56687658ad10c3216/charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca", size = 107438, upload-time = "2025-08-09T07:57:25.287Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[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 = "coverage" +version = "7.10.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/70/025b179c993f019105b79575ac6edb5e084fb0f0e63f15cdebef4e454fb5/coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90", size = 823736, upload-time = "2025-08-29T15:35:16.668Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/1d/2e64b43d978b5bd184e0756a41415597dfef30fcbd90b747474bd749d45f/coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356", size = 217025, upload-time = "2025-08-29T15:32:57.169Z" }, + { url = "https://files.pythonhosted.org/packages/23/62/b1e0f513417c02cc10ef735c3ee5186df55f190f70498b3702d516aad06f/coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301", size = 217419, upload-time = "2025-08-29T15:32:59.908Z" }, + { url = "https://files.pythonhosted.org/packages/e7/16/b800640b7a43e7c538429e4d7223e0a94fd72453a1a048f70bf766f12e96/coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460", size = 244180, upload-time = "2025-08-29T15:33:01.608Z" }, + { url = "https://files.pythonhosted.org/packages/fb/6f/5e03631c3305cad187eaf76af0b559fff88af9a0b0c180d006fb02413d7a/coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd", size = 245992, upload-time = "2025-08-29T15:33:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a1/f30ea0fb400b080730125b490771ec62b3375789f90af0bb68bfb8a921d7/coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb", size = 247851, upload-time = "2025-08-29T15:33:04.603Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/cfa8fee8e8ef9a6bb76c7bef039f3302f44e615d2194161a21d3d83ac2e9/coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6", size = 245891, upload-time = "2025-08-29T15:33:06.176Z" }, + { url = "https://files.pythonhosted.org/packages/93/a9/51be09b75c55c4f6c16d8d73a6a1d46ad764acca0eab48fa2ffaef5958fe/coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945", size = 243909, upload-time = "2025-08-29T15:33:07.74Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a6/ba188b376529ce36483b2d585ca7bdac64aacbe5aa10da5978029a9c94db/coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e", size = 244786, upload-time = "2025-08-29T15:33:08.965Z" }, + { url = "https://files.pythonhosted.org/packages/d0/4c/37ed872374a21813e0d3215256180c9a382c3f5ced6f2e5da0102fc2fd3e/coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1", size = 219521, upload-time = "2025-08-29T15:33:10.599Z" }, + { url = "https://files.pythonhosted.org/packages/8e/36/9311352fdc551dec5b973b61f4e453227ce482985a9368305880af4f85dd/coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528", size = 220417, upload-time = "2025-08-29T15:33:11.907Z" }, + { url = "https://files.pythonhosted.org/packages/d4/16/2bea27e212c4980753d6d563a0803c150edeaaddb0771a50d2afc410a261/coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f", size = 217129, upload-time = "2025-08-29T15:33:13.575Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/e7159e068831ab37e31aac0969d47b8c5ee25b7d307b51e310ec34869315/coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc", size = 217532, upload-time = "2025-08-29T15:33:14.872Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c0/246ccbea53d6099325d25cd208df94ea435cd55f0db38099dd721efc7a1f/coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a", size = 247931, upload-time = "2025-08-29T15:33:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fb/7435ef8ab9b2594a6e3f58505cc30e98ae8b33265d844007737946c59389/coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a", size = 249864, upload-time = "2025-08-29T15:33:17.434Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/d9d64e8da7bcddb094d511154824038833c81e3a039020a9d6539bf303e9/coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62", size = 251969, upload-time = "2025-08-29T15:33:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/c43ba0ef19f446d6463c751315140d8f2a521e04c3e79e5c5fe211bfa430/coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153", size = 249659, upload-time = "2025-08-29T15:33:20.407Z" }, + { url = "https://files.pythonhosted.org/packages/79/3e/53635bd0b72beaacf265784508a0b386defc9ab7fad99ff95f79ce9db555/coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5", size = 247714, upload-time = "2025-08-29T15:33:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/4c/55/0964aa87126624e8c159e32b0bc4e84edef78c89a1a4b924d28dd8265625/coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619", size = 248351, upload-time = "2025-08-29T15:33:23.105Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/6cfa9dc518c6c8e14a691c54e53a9433ba67336c760607e299bfcf520cb1/coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba", size = 219562, upload-time = "2025-08-29T15:33:24.717Z" }, + { url = "https://files.pythonhosted.org/packages/5b/18/99b25346690cbc55922e7cfef06d755d4abee803ef335baff0014268eff4/coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e", size = 220453, upload-time = "2025-08-29T15:33:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ed/81d86648a07ccb124a5cf1f1a7788712b8d7216b593562683cd5c9b0d2c1/coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c", size = 219127, upload-time = "2025-08-29T15:33:27.777Z" }, + { url = "https://files.pythonhosted.org/packages/26/06/263f3305c97ad78aab066d116b52250dd316e74fcc20c197b61e07eb391a/coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea", size = 217324, upload-time = "2025-08-29T15:33:29.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/60/1e1ded9a4fe80d843d7d53b3e395c1db3ff32d6c301e501f393b2e6c1c1f/coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634", size = 217560, upload-time = "2025-08-29T15:33:30.748Z" }, + { url = "https://files.pythonhosted.org/packages/b8/25/52136173c14e26dfed8b106ed725811bb53c30b896d04d28d74cb64318b3/coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6", size = 249053, upload-time = "2025-08-29T15:33:32.041Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1d/ae25a7dc58fcce8b172d42ffe5313fc267afe61c97fa872b80ee72d9515a/coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9", size = 251802, upload-time = "2025-08-29T15:33:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7a/1f561d47743710fe996957ed7c124b421320f150f1d38523d8d9102d3e2a/coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c", size = 252935, upload-time = "2025-08-29T15:33:34.909Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ad/8b97cd5d28aecdfde792dcbf646bac141167a5cacae2cd775998b45fabb5/coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a", size = 250855, upload-time = "2025-08-29T15:33:36.922Z" }, + { url = "https://files.pythonhosted.org/packages/33/6a/95c32b558d9a61858ff9d79580d3877df3eb5bc9eed0941b1f187c89e143/coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5", size = 248974, upload-time = "2025-08-29T15:33:38.175Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9c/8ce95dee640a38e760d5b747c10913e7a06554704d60b41e73fdea6a1ffd/coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972", size = 250409, upload-time = "2025-08-29T15:33:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/04/12/7a55b0bdde78a98e2eb2356771fd2dcddb96579e8342bb52aa5bc52e96f0/coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d", size = 219724, upload-time = "2025-08-29T15:33:41.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/4a/32b185b8b8e327802c9efce3d3108d2fe2d9d31f153a0f7ecfd59c773705/coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629", size = 220536, upload-time = "2025-08-29T15:33:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/08/3a/d5d8dc703e4998038c3099eaf77adddb00536a3cec08c8dcd556a36a3eb4/coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80", size = 219171, upload-time = "2025-08-29T15:33:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/bd/e7/917e5953ea29a28c1057729c1d5af9084ab6d9c66217523fd0e10f14d8f6/coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6", size = 217351, upload-time = "2025-08-29T15:33:45.438Z" }, + { url = "https://files.pythonhosted.org/packages/eb/86/2e161b93a4f11d0ea93f9bebb6a53f113d5d6e416d7561ca41bb0a29996b/coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80", size = 217600, upload-time = "2025-08-29T15:33:47.269Z" }, + { url = "https://files.pythonhosted.org/packages/0e/66/d03348fdd8df262b3a7fb4ee5727e6e4936e39e2f3a842e803196946f200/coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003", size = 248600, upload-time = "2025-08-29T15:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/73/dd/508420fb47d09d904d962f123221bc249f64b5e56aa93d5f5f7603be475f/coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27", size = 251206, upload-time = "2025-08-29T15:33:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1f/9020135734184f439da85c70ea78194c2730e56c2d18aee6e8ff1719d50d/coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4", size = 252478, upload-time = "2025-08-29T15:33:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a4/3d228f3942bb5a2051fde28c136eea23a761177dc4ff4ef54533164ce255/coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d", size = 250637, upload-time = "2025-08-29T15:33:53.67Z" }, + { url = "https://files.pythonhosted.org/packages/36/e3/293dce8cdb9a83de971637afc59b7190faad60603b40e32635cbd15fbf61/coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc", size = 248529, upload-time = "2025-08-29T15:33:55.022Z" }, + { url = "https://files.pythonhosted.org/packages/90/26/64eecfa214e80dd1d101e420cab2901827de0e49631d666543d0e53cf597/coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc", size = 250143, upload-time = "2025-08-29T15:33:56.386Z" }, + { url = "https://files.pythonhosted.org/packages/3e/70/bd80588338f65ea5b0d97e424b820fb4068b9cfb9597fbd91963086e004b/coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e", size = 219770, upload-time = "2025-08-29T15:33:58.063Z" }, + { url = "https://files.pythonhosted.org/packages/a7/14/0b831122305abcc1060c008f6c97bbdc0a913ab47d65070a01dc50293c2b/coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32", size = 220566, upload-time = "2025-08-29T15:33:59.766Z" }, + { url = "https://files.pythonhosted.org/packages/83/c6/81a83778c1f83f1a4a168ed6673eeedc205afb562d8500175292ca64b94e/coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2", size = 219195, upload-time = "2025-08-29T15:34:01.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/ccccf4bf116f9517275fa85047495515add43e41dfe8e0bef6e333c6b344/coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b", size = 218059, upload-time = "2025-08-29T15:34:02.91Z" }, + { url = "https://files.pythonhosted.org/packages/92/97/8a3ceff833d27c7492af4f39d5da6761e9ff624831db9e9f25b3886ddbca/coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393", size = 218287, upload-time = "2025-08-29T15:34:05.106Z" }, + { url = "https://files.pythonhosted.org/packages/92/d8/50b4a32580cf41ff0423777a2791aaf3269ab60c840b62009aec12d3970d/coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27", size = 259625, upload-time = "2025-08-29T15:34:06.575Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7e/6a7df5a6fb440a0179d94a348eb6616ed4745e7df26bf2a02bc4db72c421/coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df", size = 261801, upload-time = "2025-08-29T15:34:08.006Z" }, + { url = "https://files.pythonhosted.org/packages/3a/4c/a270a414f4ed5d196b9d3d67922968e768cd971d1b251e1b4f75e9362f75/coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb", size = 264027, upload-time = "2025-08-29T15:34:09.806Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8b/3210d663d594926c12f373c5370bf1e7c5c3a427519a8afa65b561b9a55c/coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282", size = 261576, upload-time = "2025-08-29T15:34:11.585Z" }, + { url = "https://files.pythonhosted.org/packages/72/d0/e1961eff67e9e1dba3fc5eb7a4caf726b35a5b03776892da8d79ec895775/coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4", size = 259341, upload-time = "2025-08-29T15:34:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/3a/06/d6478d152cd189b33eac691cba27a40704990ba95de49771285f34a5861e/coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21", size = 260468, upload-time = "2025-08-29T15:34:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/ed/73/737440247c914a332f0b47f7598535b29965bf305e19bbc22d4c39615d2b/coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0", size = 220429, upload-time = "2025-08-29T15:34:16.394Z" }, + { url = "https://files.pythonhosted.org/packages/bd/76/b92d3214740f2357ef4a27c75a526eb6c28f79c402e9f20a922c295c05e2/coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5", size = 221493, upload-time = "2025-08-29T15:34:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/6dcb29c599c8a1f654ec6cb68d76644fe635513af16e932d2d4ad1e5ac6e/coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b", size = 219757, upload-time = "2025-08-29T15:34:19.248Z" }, + { url = "https://files.pythonhosted.org/packages/d3/aa/76cf0b5ec00619ef208da4689281d48b57f2c7fde883d14bf9441b74d59f/coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e", size = 217331, upload-time = "2025-08-29T15:34:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/65/91/8e41b8c7c505d398d7730206f3cbb4a875a35ca1041efc518051bfce0f6b/coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb", size = 217607, upload-time = "2025-08-29T15:34:22.433Z" }, + { url = "https://files.pythonhosted.org/packages/87/7f/f718e732a423d442e6616580a951b8d1ec3575ea48bcd0e2228386805e79/coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034", size = 248663, upload-time = "2025-08-29T15:34:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/e6/52/c1106120e6d801ac03e12b5285e971e758e925b6f82ee9b86db3aa10045d/coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1", size = 251197, upload-time = "2025-08-29T15:34:25.906Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ec/3a8645b1bb40e36acde9c0609f08942852a4af91a937fe2c129a38f2d3f5/coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a", size = 252551, upload-time = "2025-08-29T15:34:27.337Z" }, + { url = "https://files.pythonhosted.org/packages/a1/70/09ecb68eeb1155b28a1d16525fd3a9b65fbe75337311a99830df935d62b6/coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb", size = 250553, upload-time = "2025-08-29T15:34:29.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/80/47df374b893fa812e953b5bc93dcb1427a7b3d7a1a7d2db33043d17f74b9/coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d", size = 248486, upload-time = "2025-08-29T15:34:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/9f98640979ecee1b0d1a7164b589de720ddf8100d1747d9bbdb84be0c0fb/coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747", size = 249981, upload-time = "2025-08-29T15:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/1f/55/eeb6603371e6629037f47bd25bef300387257ed53a3c5fdb159b7ac8c651/coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5", size = 220054, upload-time = "2025-08-29T15:34:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/15/d1/a0912b7611bc35412e919a2cd59ae98e7ea3b475e562668040a43fb27897/coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713", size = 220851, upload-time = "2025-08-29T15:34:35.651Z" }, + { url = "https://files.pythonhosted.org/packages/ef/2d/11880bb8ef80a45338e0b3e0725e4c2d73ffbb4822c29d987078224fd6a5/coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32", size = 219429, upload-time = "2025-08-29T15:34:37.16Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/1f00caad775c03a700146f55536ecd097a881ff08d310a58b353a1421be0/coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65", size = 218080, upload-time = "2025-08-29T15:34:38.919Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c4/b1c5d2bd7cc412cbeb035e257fd06ed4e3e139ac871d16a07434e145d18d/coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6", size = 218293, upload-time = "2025-08-29T15:34:40.425Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/4468d37c94724bf6ec354e4ec2f205fda194343e3e85fd2e59cec57e6a54/coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0", size = 259800, upload-time = "2025-08-29T15:34:41.996Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/f8fb351be5fee31690cd8da768fd62f1cfab33c31d9f7baba6cd8960f6b8/coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e", size = 261965, upload-time = "2025-08-29T15:34:43.61Z" }, + { url = "https://files.pythonhosted.org/packages/e8/70/65d4d7cfc75c5c6eb2fed3ee5cdf420fd8ae09c4808723a89a81d5b1b9c3/coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5", size = 264220, upload-time = "2025-08-29T15:34:45.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/3c/069df106d19024324cde10e4ec379fe2fb978017d25e97ebee23002fbadf/coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7", size = 261660, upload-time = "2025-08-29T15:34:47.288Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8a/2974d53904080c5dc91af798b3a54a4ccb99a45595cc0dcec6eb9616a57d/coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5", size = 259417, upload-time = "2025-08-29T15:34:48.779Z" }, + { url = "https://files.pythonhosted.org/packages/30/38/9616a6b49c686394b318974d7f6e08f38b8af2270ce7488e879888d1e5db/coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0", size = 260567, upload-time = "2025-08-29T15:34:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/76/16/3ed2d6312b371a8cf804abf4e14895b70e4c3491c6e53536d63fd0958a8d/coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7", size = 220831, upload-time = "2025-08-29T15:34:52.653Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e5/d38d0cb830abede2adb8b147770d2a3d0e7fecc7228245b9b1ae6c24930a/coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930", size = 221950, upload-time = "2025-08-29T15:34:54.212Z" }, + { url = "https://files.pythonhosted.org/packages/f4/51/e48e550f6279349895b0ffcd6d2a690e3131ba3a7f4eafccc141966d4dea/coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b", size = 219969, upload-time = "2025-08-29T15:34:55.83Z" }, + { url = "https://files.pythonhosted.org/packages/91/70/f73ad83b1d2fd2d5825ac58c8f551193433a7deaf9b0d00a8b69ef61cd9a/coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352", size = 217009, upload-time = "2025-08-29T15:34:57.381Z" }, + { url = "https://files.pythonhosted.org/packages/01/e8/099b55cd48922abbd4b01ddd9ffa352408614413ebfc965501e981aced6b/coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612", size = 217400, upload-time = "2025-08-29T15:34:58.985Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d1/c6bac7c9e1003110a318636fef3b5c039df57ab44abcc41d43262a163c28/coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b", size = 243835, upload-time = "2025-08-29T15:35:00.541Z" }, + { url = "https://files.pythonhosted.org/packages/01/f9/82c6c061838afbd2172e773156c0aa84a901d59211b4975a4e93accf5c89/coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144", size = 245658, upload-time = "2025-08-29T15:35:02.135Z" }, + { url = "https://files.pythonhosted.org/packages/81/6a/35674445b1d38161148558a3ff51b0aa7f0b54b1def3abe3fbd34efe05bc/coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b", size = 247433, upload-time = "2025-08-29T15:35:03.777Z" }, + { url = "https://files.pythonhosted.org/packages/18/27/98c99e7cafb288730a93535092eb433b5503d529869791681c4f2e2012a8/coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862", size = 245315, upload-time = "2025-08-29T15:35:05.629Z" }, + { url = "https://files.pythonhosted.org/packages/09/05/123e0dba812408c719c319dea05782433246f7aa7b67e60402d90e847545/coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2", size = 243385, upload-time = "2025-08-29T15:35:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/67/52/d57a42502aef05c6325f28e2e81216c2d9b489040132c18725b7a04d1448/coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78", size = 244343, upload-time = "2025-08-29T15:35:09.55Z" }, + { url = "https://files.pythonhosted.org/packages/6b/22/7f6fad7dbb37cf99b542c5e157d463bd96b797078b1ec506691bc836f476/coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c", size = 219530, upload-time = "2025-08-29T15:35:11.167Z" }, + { url = "https://files.pythonhosted.org/packages/62/30/e2fda29bfe335026027e11e6a5e57a764c9df13127b5cf42af4c3e99b937/coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf", size = 220432, upload-time = "2025-08-29T15:35:12.902Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/50db5379b615854b5cf89146f8f5bd1d5a9693d7f3a987e269693521c404/coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3", size = 208986, upload-time = "2025-08-29T15:35:14.506Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, +] + +[[package]] +name = "flake8" +version = "7.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" }, +] + +[[package]] +name = "identify" +version = "2.6.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "kilm" +version = "0.5.0" +source = { editable = "." } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pathlib" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "requests" }, + { name = "rich" }, + { name = "typer" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "flake8" }, + { name = "pre-commit" }, + { name = "pyrefly" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=22.0.0" }, + { name = "click", specifier = ">=8.0" }, + { name = "flake8", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "jinja2", specifier = ">=3.1.6" }, + { name = "packaging", specifier = ">=25.0" }, + { name = "pathlib", specifier = ">=1.0.1" }, + { name = "pathspec", specifier = ">=0.12.1" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5.0" }, + { name = "pyrefly", marker = "extra == 'dev'", specifier = ">=0.31.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "questionary", specifier = ">=1.10.0" }, + { name = "requests", specifier = ">=2.32.4" }, + { name = "rich", specifier = ">=14.0.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" }, + { name = "typer", specifier = ">=0.17.4" }, +] +provides-extras = ["dev"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.10'", +] +dependencies = [ + { name = "mdurl", marker = "python_full_version >= '3.10'" }, +] +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 = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, + { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, + { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, + { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, + { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, + { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, + { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, + { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pathlib" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/aa/9b065a76b9af472437a0059f77e8f962fe350438b927cb80184c32f075eb/pathlib-1.0.1.tar.gz", hash = "sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f", size = 49298, upload-time = "2014-09-03T15:41:57.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/f9/690a8600b93c332de3ab4a344a4ac34f00c8f104917061f779db6a918ed6/pathlib-1.0.1-py3-none-any.whl", hash = "sha256:f35f95ab8b0f59e6d354090350b44a80a80635d22efdedfa84c7ad1cf0a74147", size = 14363, upload-time = "2022-05-04T13:37:20.585Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" }, +] + +[[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 = "pycodestyle" +version = "2.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" }, +] + +[[package]] +name = "pyflakes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, +] + +[[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 = "pyrefly" +version = "0.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/c4/4b45120c1af9698909fb5e771170566959bad43530f2f0ac434674789e14/pyrefly-0.31.1.tar.gz", hash = "sha256:79609735d2683c9fda12b14e74a0643477984216297fc8786086cc29f964901d", size = 1342496, upload-time = "2025-09-05T23:06:50.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/26/1c8699516f2d67456df729df2996a5957e6f27693db00219252e7be514dd/pyrefly-0.31.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b251a502a3d7c435361fa66449b0d3eaafd9e5b7bd16802e626ae28fa8972e54", size = 6622774, upload-time = "2025-09-05T23:06:33.592Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fe/1d63007e29494894bee8e56a8e3631f5a1ecdbbf99fce233ebbc5c13e6dc/pyrefly-0.31.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3f9ca929a9a259c3c2d2d10b7639e68a6ec11f227cf0f5525a53ceb720b065ea", size = 6162225, upload-time = "2025-09-05T23:06:35.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/8f67f8a68b1d35c87b170d8c11619d01d7769c7f206d72954ed80c44c675/pyrefly-0.31.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68cf8e4b4abbdb0f50919caf1310e82fc7f2b09ba5152bfcedd8fda189b0cf42", size = 6410448, upload-time = "2025-09-05T23:06:38.228Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e6/755de7c249a6a497542f21e61e94b3afbfc096fa480678e87e7c02410b4a/pyrefly-0.31.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f7c67586f506910248cc410b5232418635604c8cc2f4d61d6432418c6a5647", size = 7204044, upload-time = "2025-09-05T23:06:39.859Z" }, + { url = "https://files.pythonhosted.org/packages/39/be/4ca0a3e8f78357e6846fb367ad00790c74c7bc80928171ce0e34d33aabc8/pyrefly-0.31.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c0d809af388144cb148eb11c6ebc33f8e5c6adafc22a7f0da99d9b555123e42", size = 6890396, upload-time = "2025-09-05T23:06:42.137Z" }, + { url = "https://files.pythonhosted.org/packages/01/1a/9c3381910d51d64a26726c6111ac5a1ee90e5de9e92b4827be352610c5b8/pyrefly-0.31.1-py3-none-win32.whl", hash = "sha256:8bde23e37fb68367b691b7491880e0ce95e150d879fe43e3a86e78f3cf911a23", size = 6383610, upload-time = "2025-09-05T23:06:44.159Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ab/f99cf8f7acebc0ddf0718ca4f33786e78353019903e5888de133c67863f5/pyrefly-0.31.1-py3-none-win_amd64.whl", hash = "sha256:bec49618d2574ccd200de515480c4b60740926f3a7a4b1804fdae49b72665719", size = 6800276, upload-time = "2025-09-05T23:06:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/02/53/b02648724846a3bd676410261f2d980a260a16e337fe978028429fc712d1/pyrefly-0.31.1-py3-none-win_arm64.whl", hash = "sha256:370d2b7fab9cab0de937f44638cf6666322e27b6d2c84c4b82cba0e868c6d437", size = 6421184, upload-time = "2025-09-05T23:06:48.218Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/30/4c/f883ab8f0daad69f47efdf95f55a66b51a8b939c430dadce0611508d9e99/pytest_cov-6.3.0.tar.gz", hash = "sha256:35c580e7800f87ce892e687461166e1ac2bcb8fb9e13aea79032518d6e503ff2", size = 70398, upload-time = "2025-09-06T15:40:14.361Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/b4/bb7263e12aade3842b938bc5c6958cae79c5ee18992f9b9349019579da0f/pytest_cov-6.3.0-py3-none-any.whl", hash = "sha256:440db28156d2468cafc0415b4f8e50856a0d11faefa38f30906048fe490f1749", size = 25115, upload-time = "2025-09-06T15:40:12.44Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, + { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, + { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "markdown-it-py", version = "4.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "ruff" +version = "0.12.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a8/f0/e0965dd709b8cabe6356811c0ee8c096806bb57d20b5019eb4e48a117410/ruff-0.12.12.tar.gz", hash = "sha256:b86cd3415dbe31b3b46a71c598f4c4b2f550346d1ccf6326b347cc0c8fd063d6", size = 5359915, upload-time = "2025-09-04T16:50:18.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/79/8d3d687224d88367b51c7974cec1040c4b015772bfbeffac95face14c04a/ruff-0.12.12-py3-none-linux_armv6l.whl", hash = "sha256:de1c4b916d98ab289818e55ce481e2cacfaad7710b01d1f990c497edf217dafc", size = 12116602, upload-time = "2025-09-04T16:49:18.892Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c3/6e599657fe192462f94861a09aae935b869aea8a1da07f47d6eae471397c/ruff-0.12.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7acd6045e87fac75a0b0cdedacf9ab3e1ad9d929d149785903cff9bb69ad9727", size = 12868393, upload-time = "2025-09-04T16:49:23.043Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d2/9e3e40d399abc95336b1843f52fc0daaceb672d0e3c9290a28ff1a96f79d/ruff-0.12.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:abf4073688d7d6da16611f2f126be86523a8ec4343d15d276c614bda8ec44edb", size = 12036967, upload-time = "2025-09-04T16:49:26.04Z" }, + { url = "https://files.pythonhosted.org/packages/e9/03/6816b2ed08836be272e87107d905f0908be5b4a40c14bfc91043e76631b8/ruff-0.12.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:968e77094b1d7a576992ac078557d1439df678a34c6fe02fd979f973af167577", size = 12276038, upload-time = "2025-09-04T16:49:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d5/707b92a61310edf358a389477eabd8af68f375c0ef858194be97ca5b6069/ruff-0.12.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42a67d16e5b1ffc6d21c5f67851e0e769517fb57a8ebad1d0781b30888aa704e", size = 11901110, upload-time = "2025-09-04T16:49:32.07Z" }, + { url = "https://files.pythonhosted.org/packages/9d/3d/f8b1038f4b9822e26ec3d5b49cf2bc313e3c1564cceb4c1a42820bf74853/ruff-0.12.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216ec0a0674e4b1214dcc998a5088e54eaf39417327b19ffefba1c4a1e4971e", size = 13668352, upload-time = "2025-09-04T16:49:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/98/0e/91421368ae6c4f3765dd41a150f760c5f725516028a6be30e58255e3c668/ruff-0.12.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:59f909c0fdd8f1dcdbfed0b9569b8bf428cf144bec87d9de298dcd4723f5bee8", size = 14638365, upload-time = "2025-09-04T16:49:38.892Z" }, + { url = "https://files.pythonhosted.org/packages/74/5d/88f3f06a142f58ecc8ecb0c2fe0b82343e2a2b04dcd098809f717cf74b6c/ruff-0.12.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ac93d87047e765336f0c18eacad51dad0c1c33c9df7484c40f98e1d773876f5", size = 14060812, upload-time = "2025-09-04T16:49:42.732Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/8962e7ddd2e81863d5c92400820f650b86f97ff919c59836fbc4c1a6d84c/ruff-0.12.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:01543c137fd3650d322922e8b14cc133b8ea734617c4891c5a9fccf4bfc9aa92", size = 13050208, upload-time = "2025-09-04T16:49:46.434Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/8deb52d48a9a624fd37390555d9589e719eac568c020b27e96eed671f25f/ruff-0.12.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afc2fa864197634e549d87fb1e7b6feb01df0a80fd510d6489e1ce8c0b1cc45", size = 13311444, upload-time = "2025-09-04T16:49:49.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/81/de5a29af7eb8f341f8140867ffb93f82e4fde7256dadee79016ac87c2716/ruff-0.12.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0c0945246f5ad776cb8925e36af2438e66188d2b57d9cf2eed2c382c58b371e5", size = 13279474, upload-time = "2025-09-04T16:49:53.465Z" }, + { url = "https://files.pythonhosted.org/packages/7f/14/d9577fdeaf791737ada1b4f5c6b59c21c3326f3f683229096cccd7674e0c/ruff-0.12.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a0fbafe8c58e37aae28b84a80ba1817f2ea552e9450156018a478bf1fa80f4e4", size = 12070204, upload-time = "2025-09-04T16:49:56.882Z" }, + { url = "https://files.pythonhosted.org/packages/77/04/a910078284b47fad54506dc0af13839c418ff704e341c176f64e1127e461/ruff-0.12.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b9c456fb2fc8e1282affa932c9e40f5ec31ec9cbb66751a316bd131273b57c23", size = 11880347, upload-time = "2025-09-04T16:49:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/30185fcb0e89f05e7ea82e5817b47798f7fa7179863f9d9ba6fd4fe1b098/ruff-0.12.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f12856123b0ad0147d90b3961f5c90e7427f9acd4b40050705499c98983f489", size = 12891844, upload-time = "2025-09-04T16:50:02.591Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/28a8dacce4855e6703dcb8cdf6c1705d0b23dd01d60150786cd55aa93b16/ruff-0.12.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:26a1b5a2bf7dd2c47e3b46d077cd9c0fc3b93e6c6cc9ed750bd312ae9dc302ee", size = 13360687, upload-time = "2025-09-04T16:50:05.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/fa/05b6428a008e60f79546c943e54068316f32ec8ab5c4f73e4563934fbdc7/ruff-0.12.12-py3-none-win32.whl", hash = "sha256:173be2bfc142af07a01e3a759aba6f7791aa47acf3604f610b1c36db888df7b1", size = 12052870, upload-time = "2025-09-04T16:50:09.121Z" }, + { url = "https://files.pythonhosted.org/packages/85/60/d1e335417804df452589271818749d061b22772b87efda88354cf35cdb7a/ruff-0.12.12-py3-none-win_amd64.whl", hash = "sha256:e99620bf01884e5f38611934c09dd194eb665b0109104acae3ba6102b600fd0d", size = 13178016, upload-time = "2025-09-04T16:50:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/28/7e/61c42657f6e4614a4258f1c3b0c5b93adc4d1f8575f5229d1906b483099b/ruff-0.12.12-py3-none-win_arm64.whl", hash = "sha256:2a8199cab4ce4d72d158319b63370abf60991495fb733db96cd923a34c52d093", size = 12256762, upload-time = "2025-09-04T16:50:15.737Z" }, +] + +[[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 = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typer" +version = "0.17.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/e8/2a73ccf9874ec4c7638f172efc8972ceab13a0e3480b389d6ed822f7a822/typer-0.17.4.tar.gz", hash = "sha256:b77dc07d849312fd2bb5e7f20a7af8985c7ec360c45b051ed5412f64d8dc1580", size = 103734, upload-time = "2025-09-05T18:14:40.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/72/6b3e70d32e89a5cbb6a4513726c1ae8762165b027af569289e19ec08edd8/typer-0.17.4-py3-none-any.whl", hash = "sha256:015534a6edaa450e7007eba705d5c18c3349dcea50a6ad79a5ed530967575824", size = 46643, upload-time = "2025-09-05T18:14:39.166Z" }, +] + +[[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 = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +]