diff --git a/.cursor/rules/copier-template.mdc b/.cursor/rules/copier-template.mdc new file mode 100644 index 0000000..0519e5d --- /dev/null +++ b/.cursor/rules/copier-template.mdc @@ -0,0 +1,68 @@ +--- +description: Copier template development patterns and conventions +globs: ["copier.yaml", "template/**"] +alwaysApply: true +--- + +# Copier Template Development + +This is a Copier template project, not a Python application. + +## Template File Conventions + +### File Extensions +- `.jinja` suffix → file will be rendered with Jinja2 +- No `.jinja` suffix → file copied as-is +- Conditional filenames: `{% if condition %}filename{% endif %}.jinja` + +### Directory Names +- `{{variable}}/` → directory name from user input +- `{% if condition %}dirname{% endif %}/` → conditional directory + +## Jinja2 Patterns + +### Variables +- All variables come from `copier.yaml` questions +- Access with `{{ variable_name }}` +- Use filters: `{{ name | lower | replace(' ', '_') }}` + +### Conditionals +```jinja +{% if include_feature %} +content only when feature enabled +{% endif %} + +{%- if condition -%} {# strips surrounding whitespace #} +``` + +### Whitespace Control +- `{%-` strips whitespace before +- `-%}` strips whitespace after +- Use for clean output without extra blank lines + +## copier.yaml Structure + +### Question Types +- `type: str` - text input +- `type: bool` - yes/no +- `type: int` - number +- `choices:` - selection list + +### Computed Variables +```yaml +_hidden_var: + default: "{{ other_var | filter }}" + when: false # Not shown to user +``` + +### Validation +```yaml +variable: + validator: "{% if not variable %}Error message{% endif %}" +``` + +## Testing Templates + +- Tests use `copier.run_copy()` with `unsafe=True` +- Test files live in `tests/`, not `template/tests/` +- Test both enabled and disabled feature combinations diff --git a/.cursor/rules/development.mdc b/.cursor/rules/development.mdc index 7fa0f11..879e91a 100644 --- a/.cursor/rules/development.mdc +++ b/.cursor/rules/development.mdc @@ -1,61 +1,44 @@ --- -description: Development workflow, commands, and tooling for the project +description: Development workflow for the Copier template project alwaysApply: true --- # Development Workflow ## Package Management -- Use `uv` for all dependency management and Python environment operations -- Never use `pip` directly; always use `uv` commands +- Use `uv` for dependency management - Sync dependencies: `uv sync --all-extras` -## Task Runner -- Use `poe` (poethepoet) for running project tasks -- All poe tasks are defined in `scripts/app.toml` -- Run tasks with: `uv run poe ` - ## Available Commands -### Dependency Management -```bash -uv run poe sync # Sync all dependencies -uv run poe install-hooks # Install pre-commit hooks -``` - -### Code Quality ```bash +uv run poe sync # Sync dependencies uv run poe format # Format code with ruff -uv run poe lint # Lint and auto-fix with ruff -uv run poe check # Run type checker (mypy) -uv run poe test # Run tests with pytest +uv run poe lint # Lint with ruff +uv run poe check # Type check with mypy +uv run poe test # Run template tests +uv run poe flc # Format + Lint + Check +uv run poe flct # Format + Lint + Check + Test +uv run poe generate # Generate test project to /tmp ``` -### Combined Workflows -```bash -uv run poe flc # Format → Lint → Check (recommended before commits) -uv run poe flct # Format → Lint → Check → Test (full validation) -``` +## Testing Template Changes -## Code Quality Tools +1. **Unit tests**: `uv run poe test` + - Tests template generation with various options + - Verifies generated files exist and have correct content + - Validates generated project passes linting/tests -### Ruff -- Line length: 88 characters (Black default) -- Auto-fix enabled by default -- Comprehensive linting ruleset (`select = ["ALL"]`) -- Specific ignores: ANN (type hints), D (docstrings), COM812, ISC001 +2. **Manual testing**: `uv run poe generate` + - Creates project at `/tmp/test-project` + - Inspect generated files manually -### MyPy -- Strict mode enabled -- Handles all type checking (not ruff) +## Code Quality -### Pytest -- Code coverage enabled by default -- Reports show missing coverage -- Fails if tests don't cover changes +### What Gets Checked +- `tests/` - Python test code (ruff, mypy) +- `template/` - Excluded from linting (contains Jinja templates) -## Workflow Best Practices -- Run `uv run poe flc` before committing changes -- Run `uv run poe flct` for complete validation -- Use pre-commit hooks for automated checks -- Address linter errors immediately (don't ignore or suppress) +### Pre-commit +- Run `uv run poe flc` before committing +- CI runs full validation on PRs diff --git a/.cursor/rules/testing.mdc b/.cursor/rules/testing.mdc index e25bc40..1802fc2 100644 --- a/.cursor/rules/testing.mdc +++ b/.cursor/rules/testing.mdc @@ -1,74 +1,80 @@ --- -description: Testing standards and best practices using pytest -globs: ["tests/**/*.py", "**/test_*.py"] +description: Testing patterns for Copier template projects +globs: ["tests/**/*.py"] alwaysApply: false --- -# Testing Guidelines - -## Test Framework -- Use `pytest` as the testing framework (not `unittest`) -- Run tests with: `uv run poe test` -- Tests are located in the `tests/` directory +# Template Testing ## Test Structure -### Fixtures -- **Use fixtures when possible** for test setup and teardown -- Prefer fixtures over setup/teardown methods -- Use fixture scopes appropriately (`function`, `class`, `module`, `session`) -- Name fixtures descriptively based on what they provide -- **Use `conftest.py` for shared fixtures**: - - Place global fixtures in `tests/conftest.py` for all tests - - Use nested `conftest.py` files in subdirectories for package-specific fixtures - - Fixtures in `conftest.py` are automatically discovered by pytest - -### Test Organization -- One test file per module: `test_.py` -- **Avoid large test files with independent content** - - When testing different things, create separate files unless there's a reason to keep them together - - Split tests by functionality, feature, or component - - Keep test files focused and manageable in size -- Group related tests in classes when it improves organization -- Use descriptive test names that explain what is being tested -- Follow the Arrange-Act-Assert (AAA) pattern - -### Test Best Practices -- Keep tests focused and test one thing at a time -- Use parametrize for testing multiple scenarios -- Mock external dependencies and I/O operations -- Ensure tests are independent and can run in any order -- Write tests that are fast and deterministic - -## Example Patterns +Tests verify that the Copier template generates valid projects. ```python -import pytest +from copier import run_copy +def test_feature(template_path: Path, tmp_path: Path) -> None: + run_copy( + str(template_path), + tmp_path, + data={"project_slug": "test", ...}, + unsafe=True, # Required for local templates + ) -@pytest.fixture -def sample_config(): - """Fixture providing test configuration.""" - return {"key": "value"} + # Assert generated files exist and have correct content + assert (tmp_path / "expected_file.py").exists() +``` + +## Common Test Patterns + +### Testing Conditional Features +```python +def test_feature_enabled(template_path, tmp_path): + run_copy(..., data={..., "include_feature": True}) + assert (tmp_path / "feature_file").exists() + +def test_feature_disabled(template_path, tmp_path): + run_copy(..., data={..., "include_feature": False}) + assert not (tmp_path / "feature_file").exists() +``` + +### Testing Generated Content +```python +def test_variable_substitution(template_path, tmp_path): + run_copy(..., data={"project_slug": "my_project"}) + content = (tmp_path / "pyproject.toml").read_text() + assert 'name = "my_project"' in content +``` +### Testing Generated Project Validity +```python +def test_project_passes_validation(template_path, tmp_path): + run_copy(...) -def test_feature_with_fixture(sample_config): - """Test using a fixture.""" - assert sample_config["key"] == "value" + # Sync deps and run checks + subprocess.run(["uv", "sync"], cwd=tmp_path, check=True) + subprocess.run(["uv", "run", "ruff", "check", "."], cwd=tmp_path, check=True) + subprocess.run(["uv", "run", "mypy", "."], cwd=tmp_path, check=True) +``` +## Fixtures -@pytest.mark.parametrize("input,expected", [ - (1, 2), - (2, 4), - (3, 6), -]) -def test_parametrized(input, expected): - """Test multiple scenarios with parametrize.""" - assert input * 2 == expected +```python +@pytest.fixture +def template_path() -> Path: + return Path(__file__).parent.parent + +@pytest.fixture +def default_answers() -> dict[str, str | bool]: + return { + "project_slug": "test_project", + # ... all required answers + } ``` -## Coverage -- Code coverage is enabled by default -- Aim for meaningful coverage, not just high percentages -- Focus on testing critical paths and edge cases -- Coverage reports show missing lines to guide test writing +## Best Practices + +- Test all boolean option combinations +- Verify generated project passes its own tests +- Check both file existence and content +- Use `tmp_path` fixture for isolation diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ced6980..29590d0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,6 +1,8 @@ -name: CI checks +name: CI - Template Validation on: + push: + branches: [main] pull_request: defaults: @@ -8,18 +10,102 @@ defaults: shell: bash jobs: - format-lint-test: + lint-and-test: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.12", "3.13"] steps: - name: 🛎️ Checkout uses: actions/checkout@v4 - - name: ✅ validate code - env: - PY_VERSION: ${{ matrix.python-version }} + - name: 📦 Install uv + uses: astral-sh/setup-uv@v6 - uses: ./.github/actions/validation + - name: 🐍 Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: 🦾 Install dependencies + run: uv sync --all-extras + + - name: 💅 Format check + run: uv run ruff format --check . + + - name: 🔍 Lint + run: uv run ruff check . + + - name: 📝 Type check + run: uv run mypy . + + - name: 🧪 Run template tests + run: uv run pytest tests/ -v + + test-generated-project: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + include_docker: [true, false] + include_github_actions: [true, false] + + steps: + - name: 🛎️ Checkout + uses: actions/checkout@v4 + + - name: 📦 Install uv + uses: astral-sh/setup-uv@v6 + + - name: 🐍 Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: 🦾 Install copier + run: uv tool install copier + + - name: 🏗️ Generate project from template + run: | + copier copy . /tmp/generated-project \ + --defaults \ + --data "project_slug=generated_test_project" \ + --data "description=A generated test project" \ + --data "author_name=CI Bot" \ + --data "author_email=ci@example.com" \ + --data "github_username=cibot" \ + --data "python_version=${{ matrix.python-version }}" \ + --data "license=MIT" \ + --data "include_docker=${{ matrix.include_docker }}" \ + --data "include_github_actions=${{ matrix.include_github_actions }}" \ + --data "include_cursor_rules=false" \ + --trust + + - name: 📂 List generated files + run: find /tmp/generated-project -type f | head -50 + + - name: 🦾 Install generated project dependencies + run: | + cd /tmp/generated-project + uv sync --all-extras + + - name: 💅 Format check generated project + run: | + cd /tmp/generated-project + uv run ruff format --check . + + - name: 🔍 Lint generated project + run: | + cd /tmp/generated-project + uv run ruff check . + + - name: 📝 Type check generated project + run: | + cd /tmp/generated-project + uv run mypy . + + - name: 🧪 Run generated project tests + run: | + cd /tmp/generated-project + uv run pytest tests/ -v diff --git a/AGENTS.md b/AGENTS.md index 57c9b77..bae45ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,47 +1,60 @@ # Agent Instructions -## Working with Rules - -- **Suggest rule additions proactively** - - If you notice a repeated pattern, preference, or guideline that could be formalized - - If a request seems like it should be part of the project's standards - - **Do not add rules without asking** - only suggest and explain where it would fit - - Let the user decide whether to add it and make the changes themselves - -## General Coding Principles - -### SOLID Principles -Follow SOLID principles in all code: -- **Single Responsibility**: Each function/class should have one clear purpose -- **Open/Closed**: Open for extension, closed for modification -- **Liskov Substitution**: Subtypes must be substitutable for their base types -- **Interface Segregation**: Prefer small, focused interfaces over large ones -- **Dependency Inversion**: Depend on abstractions, not concrete implementations - -### Code Organization -- **Prefer functions over classes** when possible - - Use functions for simple, stateless operations - - Only introduce classes when you need state management or complex behavior - - Favor composition over inheritance - -- **Avoid global variables** - - Pass dependencies explicitly through function parameters - - Use dependency injection patterns when needed - - Keep state localized and controlled - -- **Configuration management** - - Use `pydantic-settings` for application configuration - - Define settings as Pydantic models with proper validation - - Load from environment variables when appropriate - - Keep configuration immutable after initialization - -- **Data models** - - Use `dataclasses` for internal domain models (lightweight, no validation overhead) - - Use `Pydantic` models for external boundaries (APIs, user input, file parsing) - - Validate at system boundaries, trust internal data - -## Best Practices -- Write small, focused functions that do one thing well -- Keep functions pure when possible (no side effects) -- Make dependencies explicit through parameters -- Use type hints to make contracts clear +This is a **Copier template project**, not a Python application. + +## Project Purpose + +This repository contains a [Copier](https://copier.readthedocs.io/) template that generates Python projects. When working here, you are: +- Editing **template source files**, not application code +- Writing **Jinja2 templates** that will be rendered into real files +- Testing **template generation**, not application logic + +## Key Concepts + +### Template Structure +``` +python-template/ +├── copier.yaml # Template configuration & questions +├── template/ # Files that get copied to generated projects +│ ├── *.jinja # Jinja templates (rendered) +│ └── * # Static files (copied as-is) +├── tests/ # Tests for template generation +└── .cursor/rules/ # Rules for THIS project (not copied) +``` + +### File Naming Conventions +- `file.py.jinja` → Rendered to `file.py` in generated project +- `{% if condition %}file{% endif %}.jinja` → Conditionally included +- `{{variable}}/` → Directory name from user input + +## Working with Templates + +### Jinja Syntax +- Use `{{ variable }}` for variable substitution +- Use `{% if condition %}...{% endif %}` for conditionals +- Use `{%- ... -%}` to strip whitespace +- Variables come from `copier.yaml` questions + +### Testing Changes +```bash +uv run poe test # Run all template tests +uv run poe generate # Generate test project to /tmp +``` + +### Common Patterns +```jinja +{# Conditional content #} +{% if include_feature %} +feature-specific content +{% endif %} + +{# Derived values #} +{{ project_slug | replace('_', '-') }} +``` + +## What NOT to Do + +- ❌ Don't add application logic (APIs, databases, business logic) +- ❌ Don't install runtime dependencies in this project +- ❌ Don't confuse template tests with generated project tests +- ❌ Don't edit files in `template/` without `.jinja` if they need variables diff --git a/README.md b/README.md index ac6c3ae..a36ed42 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,191 @@ -# Python Template +# Python Copier Template -My python projects template. Trying to keep it lean. +[![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier) +[![CI - Template Validation](https://github.com/privatedumbo/python-template/actions/workflows/ci.yaml/badge.svg)](https://github.com/privatedumbo/python-template/actions/workflows/ci.yaml) + +A [Copier](https://copier.readthedocs.io/) template for modern Python projects. Batteries included with UV, Ruff, Mypy, Pytest, and more. --- -## 🎯 Core Features +## ✨ Features ### Development Tools -- 📦 UV - Ultra-fast Python package manager -- 🚀 Poe the Poet - Modern command runner with powerful features -- 💅 Ruff - Lightning-fast linter and formatter -- 🔍 Mypy - Static type checker -- 🧪 Pytest - Testing framework with fixtures and plugins +- 📦 **[UV](https://docs.astral.sh/uv/)** - Ultra-fast Python package manager +- 🚀 **[Poe the Poet](https://poethepoet.natn.io/)** - Modern task runner +- 💅 **[Ruff](https://docs.astral.sh/ruff/)** - Lightning-fast linter and formatter +- 🔍 **[Mypy](https://mypy.readthedocs.io/)** - Static type checker +- 🧪 **[Pytest](https://docs.pytest.org/)** - Testing framework with coverage + +### Infrastructure (Optional) -### Infrastructure +- 🛫 **Pre-commit hooks** - Automated code quality checks +- 🐳 **Docker** - Multi-stage builds with distroless images +- 🔄 **GitHub Actions** - CI/CD pipeline -- 🛫 Pre-commit hooks -- 🐳 Docker support with multi-stage builds and distroless images -- 🔄 GitHub Actions CI/CD pipeline +--- +## 🚀 Quick Start -## Usage +### Prerequisites -The template is based on [UV](https://docs.astral.sh/) as package manager. You need to have it installed in your system to use this template. +- Python 3.10+ +- [Copier](https://copier.readthedocs.io/) (`pipx install copier` or `uv tool install copier`) -Once you have that, you can just run: +### Generate a New Project ```bash -$ uv run poe sync +# From GitHub (recommended) +copier copy gh:privatedumbo/python-template my-awesome-project + +# From a local clone +copier copy /path/to/python-template my-awesome-project ``` -to create a virtual environment and install all the dependencies, including the development ones. +### Interactive Prompts -You also need to install the pre-commit hooks with: +Copier will ask you a series of questions to customize your project: + +| Question | Description | Default | +|----------|-------------|---------| +| `project_name` | Human-readable project name | - | +| `project_slug` | Python package name (lowercase, underscores) | Derived from project_name | +| `description` | Short project description | "A Python project" | +| `author_name` | Author's full name | - | +| `author_email` | Author's email | - | +| `github_username` | GitHub username/organization | - | +| `python_version` | Minimum Python version | 3.12 | +| `license` | Project license | MIT | +| `include_docker` | Include Docker support | Yes | +| `include_github_actions` | Include GitHub Actions CI/CD | Yes | + +### Example ```bash -$ uv run poe install-hooks +$ copier copy gh:privatedumbo/python-template my-project + +🎤 What is your project name? + My Awesome Project +🎤 Python package name (lowercase, underscores) + my_awesome_project +🎤 Short description of your project + A truly awesome Python project +🎤 Author's full name + John Doe +🎤 Author's email address + john@example.com +🎤 GitHub username or organization + johndoe +🎤 Minimum Python version + 3.12 +🎤 Project license + MIT +🎤 Include Docker support? + Yes +🎤 Include GitHub Actions CI/CD? + Yes + + create . + create .copier-answers.yml + create .gitignore + create .pre-commit-config.yaml + create .python-version + create Dockerfile + create LICENSE + create README.md + create my_awesome_project/__init__.py + create my_awesome_project/main.py + create pyproject.toml + create scripts/app.toml + create tests/__init__.py + create tests/test_core.py + ... ``` -### Formatting, Linting and Testing +--- -Format your code: +## 🔄 Updating Your Project + +When this template is updated, you can pull in the changes: ```bash -$ uv run poe format +cd my-awesome-project +copier update ``` -Lint your code: +Copier will intelligently merge template updates with your local changes. -```bash -$ uv run poe lint -``` +--- + +## 🧪 Template Development -Static checks: +### Prerequisites ```bash -$ uv run poe check +# Clone the repository +git clone https://github.com/privatedumbo/python-template.git +cd python-template + +# Install dependencies +uv sync --all-extras ``` -Test your code: +### Running Tests ```bash -$ uv run poe test +# Run all tests +uv run poe test + +# Run full validation (format, lint, check, test) +uv run poe flct ``` -Do all of the above: +### Manual Testing + +Generate a project to a temporary directory: ```bash -$ uv run poe flc # Format, Lint and Check -$ uv run poe flct # Format, Lint, Check and Test +uv run poe generate ``` -### Docker +This creates a test project at `/tmp/test-project`. + +--- -The template includes a multi stage Dockerfile, which produces an image with the code and the dependencies installed. You can build the image with: +## 📁 Template Structure -```bash -$ uv run poe dockerize ``` +python-template/ +├── copier.yaml # Copier configuration & questions +├── template/ # Template source files +│ ├── {{project_slug}}/ # Package directory (templated name) +│ │ ├── __init__.py +│ │ └── main.py.jinja +│ ├── tests/ +│ │ ├── __init__.py +│ │ └── test_core.py.jinja +│ ├── scripts/ +│ │ └── app.toml.jinja +│ ├── .github/ # GitHub Actions (conditional) +│ ├── pyproject.toml.jinja +│ ├── README.md.jinja +│ ├── Dockerfile.jinja # Docker support (conditional) +│ └── ... +├── tests/ # Template tests +│ └── test_template.py +└── pyproject.toml # Template project config +``` + +--- + +## 📝 License + +This template is licensed under the [MIT License](LICENSE). + +--- -### Github Actions +## 🙏 Acknowledgments -The template a Github Action Workflow that runs tests and linters on every pull request. +- [Copier](https://github.com/copier-org/copier) - The awesome template engine +- [UV](https://github.com/astral-sh/uv) - Ultra-fast Python package manager +- [Ruff](https://github.com/astral-sh/ruff) - Lightning-fast linter diff --git a/copier.yaml b/copier.yaml new file mode 100644 index 0000000..9335346 --- /dev/null +++ b/copier.yaml @@ -0,0 +1,141 @@ +# Copier template configuration +# See https://copier.readthedocs.io/en/stable/configuring/ + +_min_copier_version: "9.0.0" +_subdirectory: template +_answers_file: .copier-answers.yml + +_message_before_copy: | + + 🐍 Python Project Generator + ═══════════════════════════════════════════════════════ + + This template creates a modern Python project with: + • UV for fast dependency management + • Ruff for linting and formatting + • Mypy for type checking + • Pytest for testing + • Pre-commit hooks + + Let's configure your new project! + +# ───────────────────────────────────────────────────────── +# Project Information +# ───────────────────────────────────────────────────────── + +project_slug: + type: str + help: | + Project name (lowercase, underscores allowed). + This will be used as the package name and directory name. + Examples: my_project, data_pipeline, api_client + placeholder: "my_awesome_project" + validator: >- + {% if not project_slug %} + Project name is required + {% elif not project_slug | regex_search('^[a-z][a-z0-9_]*$') %} + Must start with a letter and contain only lowercase letters, numbers, and underscores + {% endif %} + +description: + type: str + help: | + A short description of your project (one line). + This appears in pyproject.toml and the README header. + default: "A Python project" + placeholder: "A blazingly fast data processing library" + +# ───────────────────────────────────────────────────────── +# Author Information +# ───────────────────────────────────────────────────────── + +author_name: + type: str + help: | + Your full name. + Used in pyproject.toml authors and LICENSE file. + placeholder: "Jane Doe" + validator: "{% if not author_name %}Author name is required{% endif %}" + +author_email: + type: str + help: | + Your email address. + Used in pyproject.toml authors field. + placeholder: "jane@example.com" + validator: >- + {% if not author_email %} + Email is required + {% elif not author_email | regex_search('^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$') %} + Please enter a valid email address + {% endif %} + +github_username: + type: str + help: | + GitHub username or organization name. + Used for repository URLs in pyproject.toml and README badges. + placeholder: "janedoe" + validator: "{% if not github_username %}GitHub username is required{% endif %}" + +# ───────────────────────────────────────────────────────── +# Python Configuration +# ───────────────────────────────────────────────────────── + +python_version: + type: str + help: | + Minimum Python version for your project. + This sets requires-python and configures tooling. + default: "3.12" + choices: + "3.11 (maintenance)": "3.11" + "3.12 (stable, recommended)": "3.12" + "3.13 (latest)": "3.13" + +license: + type: str + help: | + Open source license for your project. + Select "None" for proprietary/unlicensed projects. + default: "MIT" + choices: + "MIT (permissive, simple)": "MIT" + "Apache-2.0 (permissive, patent protection)": "Apache-2.0" + "GPL-3.0 (copyleft)": "GPL-3.0" + "BSD-3-Clause (permissive)": "BSD-3-Clause" + "None (no license file)": "None" + +# ───────────────────────────────────────────────────────── +# Optional Features +# ───────────────────────────────────────────────────────── + +include_docker: + type: bool + help: | + Include Docker support? + Adds a multi-stage Dockerfile optimized for Python/UV. + default: true + +include_github_actions: + type: bool + help: | + Include GitHub Actions CI/CD? + Adds workflows for linting, testing, and type checking. + default: true + +include_cursor_rules: + type: bool + help: | + Include Cursor AI rules? + Adds .cursor/rules/ with Python best practices for AI assistance. + default: false + +# ───────────────────────────────────────────────────────── +# Computed Variables (internal use) +# ───────────────────────────────────────────────────────── + +_current_year: + type: str + default: "{{ '%Y' | strftime }}" + when: false diff --git a/pyproject.toml b/pyproject.toml index 5c9e91c..b1b66b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,16 @@ [project] -name = "python-template" +name = "python-copier-template" version = "0.0.1" -description = "My python projects template. Trying to keep it lean." +description = "A Copier template for Python projects with UV, Ruff, Mypy, and Pytest." readme = "README.md" license = { text = "MIT" } authors = [ { name = "Franco Bocci", email = "francogbocci@gmail.com" } ] requires-python = ">=3.12" dependencies = [] -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["python_template"] - [dependency-groups] dev = [ + "copier>=9.0.0", "mypy>=1.15.0", "poethepoet>=0.35.0", "pre-commit>=4.1.0", @@ -26,33 +20,15 @@ dev = [ ] [tool.pytest.ini_options] -addopts = """\ - --cov python_template \ - --cov-report term-missing \ - --no-cov-on-fail -""" - -[tool.coverage.report] -exclude_lines = [ - # Don't complain about lines explicitly marked as uncoverable - "pragma: no cover", - # Don't complain about type checking code - "if TYPE_CHECKING:", - # Don't complain about not-implemented parts of code - "raise NotImplementedError", - # Don't complain about abstract methods, they aren't run - '@(abc\.)?abstractmethod', -] -# least covered on top, most covered on bottom -# default is "name", and to reverse the order use "-cover" -skip_covered = "true" -sort = "cover" +testpaths = ["tests"] [tool.ruff] -target-version = "py313" +target-version = "py312" output-format = "full" line-length = 88 fix = true +# Exclude template directory from linting (it contains Jinja templates) +exclude = ["template"] [tool.ruff.lint] select = ["ALL"] @@ -68,6 +44,8 @@ ignore = [ "ARG", # "Unused function argument". Fixtures are often unused. "S101", # "Use of `assert` detected" "S106", # "Possible hardcoded password" + "S603", # `subprocess` call - safe in tests + "S607", # Starting a process with a partial executable path - safe in tests "PT004", # Does not return anything, add leading underscore "PLR2004", # Magic value used in comparison ] @@ -78,6 +56,7 @@ namespace_packages = true explicit_package_bases = true plugins = [] ignore_missing_imports = true +exclude = ["template"] [tool.poe] include = "scripts/app.toml" diff --git a/scripts/app.toml b/scripts/app.toml index e348016..240d150 100644 --- a/scripts/app.toml +++ b/scripts/app.toml @@ -30,6 +30,6 @@ sequence = [ "format", "lint", "check" ] help = "`flct`: Format, lint, check, and test." sequence = [ "format", "lint", "check", "test" ] -[tool.poe.tasks.dockerize] -help = "Build docker image." -cmd = "docker build -t python-template ." +[tool.poe.tasks.generate] +help = "Generate a project from the template (for manual testing)." +cmd = "copier copy . /tmp/test-project --defaults --force" diff --git a/template/.copier-answers.yml.jinja b/template/.copier-answers.yml.jinja new file mode 100644 index 0000000..fdc0a21 --- /dev/null +++ b/template/.copier-answers.yml.jinja @@ -0,0 +1,4 @@ +# Changes here will be overwritten by Copier; NEVER EDIT MANUALLY +_src_path: {{ _copier_conf.src_path | default('') }} +_commit: {{ _copier_conf.vcs_ref | default('') }} +{{ _copier_answers | to_nice_yaml -}} diff --git a/template/.gitignore b/template/.gitignore new file mode 100644 index 0000000..3699ce3 --- /dev/null +++ b/template/.gitignore @@ -0,0 +1,218 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +.DS_Store diff --git a/template/.pre-commit-config.yaml b/template/.pre-commit-config.yaml new file mode 100644 index 0000000..7fc431b --- /dev/null +++ b/template/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: check-ast + - id: check-added-large-files + - id: check-merge-conflict + - id: check-case-conflict + - id: check-docstring-first + - id: check-json + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: mixed-line-ending + - id: trailing-whitespace + + - repo: local + hooks: + - id: flc + name: flc + language: system + entry: uv run poe flc + pass_filenames: false + types: [python] diff --git a/template/.python-version.jinja b/template/.python-version.jinja new file mode 100644 index 0000000..7773b4e --- /dev/null +++ b/template/.python-version.jinja @@ -0,0 +1 @@ +{{ python_version }} diff --git a/template/README.md.jinja b/template/README.md.jinja new file mode 100644 index 0000000..7af02c1 --- /dev/null +++ b/template/README.md.jinja @@ -0,0 +1,91 @@ +# {{ project_slug | replace('_', ' ') | title }} + +{{ description }} + +--- + +## 🎯 Core Features + +### Development Tools + +- 📦 UV - Ultra-fast Python package manager +- 🚀 Poe the Poet - Modern command runner with powerful features +- 💅 Ruff - Lightning-fast linter and formatter +- 🔍 Mypy - Static type checker +- 🧪 Pytest - Testing framework with fixtures and plugins + +### Infrastructure + +- 🛫 Pre-commit hooks +{%- if include_docker %} +- 🐳 Docker support with multi-stage builds +{%- endif %} +{%- if include_github_actions %} +- 🔄 GitHub Actions CI/CD pipeline +{%- endif %} + + +## Usage + +This project uses [UV](https://docs.astral.sh/) as package manager. You need to have it installed in your system. + +Once you have that, you can run: + +```bash +uv run poe sync +``` + +to create a virtual environment and install all the dependencies, including the development ones. + +You also need to install the pre-commit hooks with: + +```bash +uv run poe install-hooks +``` + +### Formatting, Linting and Testing + +Format your code: + +```bash +uv run poe format +``` + +Lint your code: + +```bash +uv run poe lint +``` + +Static checks: + +```bash +uv run poe check +``` + +Test your code: + +```bash +uv run poe test +``` + +Do all of the above: + +```bash +uv run poe flc # Format, Lint and Check +uv run poe flct # Format, Lint, Check and Test +``` +{% if include_docker %} +### Docker + +Build the Docker image with: + +```bash +uv run poe dockerize +``` +{% endif %} +{% if include_github_actions %} +### GitHub Actions + +This project includes a GitHub Action Workflow that runs tests and linters on every pull request. +{% endif %} diff --git a/template/pyproject.toml.jinja b/template/pyproject.toml.jinja new file mode 100644 index 0000000..62d9291 --- /dev/null +++ b/template/pyproject.toml.jinja @@ -0,0 +1,90 @@ +[project] +name = "{{ project_slug }}" +version = "0.0.1" +description = "{{ description }}" +readme = "README.md" +{%- if license != 'None' %} +license = { text = "{{ license }}" } +{%- endif %} +authors = [ { name = "{{ author_name }}", email = "{{ author_email }}" } ] +requires-python = ">={{ python_version }}" +dependencies = [] + +[project.urls] +Homepage = "https://github.com/{{ github_username }}/{{ project_slug }}" +Repository = "https://github.com/{{ github_username }}/{{ project_slug }}" +Issues = "https://github.com/{{ github_username }}/{{ project_slug }}/issues" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["{{ project_slug }}"] + +[dependency-groups] +dev = [ + "mypy>=1.15.0", + "poethepoet>=0.35.0", + "pre-commit>=4.1.0", + "pytest>=8.3.4", + "pytest-cov>=6.2.1", + "ruff>=0.9.5", +] + +[tool.pytest.ini_options] +addopts = """\ + --cov {{ project_slug }} \ + --cov-report term-missing \ + --no-cov-on-fail +""" + +[tool.coverage.report] +exclude_lines = [ + # Don't complain about lines explicitly marked as uncoverable + "pragma: no cover", + # Don't complain about type checking code + "if TYPE_CHECKING:", + # Don't complain about not-implemented parts of code + "raise NotImplementedError", + # Don't complain about abstract methods, they aren't run + '@(abc\.)?abstractmethod', +] +# least covered on top, most covered on bottom +# default is "name", and to reverse the order use "-cover" +skip_covered = "true" +sort = "cover" + +[tool.ruff] +target-version = "py{{ python_version | replace('.', '') }}" +output-format = "full" +line-length = 88 +fix = true + +[tool.ruff.lint] +select = ["ALL"] +ignore = [ + "ANN", # Type hints related, let mypy handle these. + "COM812", # "Trailing comma missing". If black is happy, we are happy. + "D", # Docstrings related, way too strict to our taste + "ISC001", # Implicit string concatenation +] + +[tool.ruff.lint.per-file-ignores] +"tests/**" = [ + "ARG", # "Unused function argument". Fixtures are often unused. + "S101", # "Use of `assert` detected" + "S106", # "Possible hardcoded password" + "PT004", # Does not return anything, add leading underscore + "PLR2004", # Magic value used in comparison +] + +[tool.mypy] +strict = true +namespace_packages = true +explicit_package_bases = true +plugins = [] +ignore_missing_imports = true + +[tool.poe] +include = "scripts/app.toml" diff --git a/template/scripts/app.toml.jinja b/template/scripts/app.toml.jinja new file mode 100644 index 0000000..aaebff4 --- /dev/null +++ b/template/scripts/app.toml.jinja @@ -0,0 +1,36 @@ +[tool.poe.tasks.sync] +help = "Sync dependencies." +cmd = "uv sync --all-extras" + +[tool.poe.tasks.install-hooks] +help = "Install pre commit hooks." +cmd = "uv run pre-commit install" + +[tool.poe.tasks.format] +help = "Format code." +cmd = "uv run ruff format ${POE_ROOT}" + +[tool.poe.tasks.lint] +help = "Lint code." +cmd = "uv run ruff check --fix ${POE_ROOT}" + +[tool.poe.tasks.check] +help = "Run type checker." +cmd = "uv run mypy ${POE_ROOT}" + +[tool.poe.tasks.test] +help = "Run tests." +cmd = "uv run pytest ${POE_ROOT}" + +[tool.poe.tasks.flc] +help = "`flc`: Format, lint, and check." +sequence = [ "format", "lint", "check" ] + +[tool.poe.tasks.flct] +help = "`flct`: Format, lint, check, and test." +sequence = [ "format", "lint", "check", "test" ] +{% if include_docker %} +[tool.poe.tasks.dockerize] +help = "Build docker image." +cmd = "docker build -t {{ project_slug }} ." +{% endif %} diff --git a/python_template/__init__.py b/template/tests/__init__.py similarity index 100% rename from python_template/__init__.py rename to template/tests/__init__.py diff --git a/tests/test_core.py b/template/tests/test_core.py.jinja similarity index 56% rename from tests/test_core.py rename to template/tests/test_core.py.jinja index 0a18280..fe9c7ac 100644 --- a/tests/test_core.py +++ b/template/tests/test_core.py.jinja @@ -1,4 +1,4 @@ -from python_template.main import foo +from {{ project_slug }}.main import foo def test_foo() -> None: diff --git a/.cursor/rules/database.mdc b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/database.mdc.jinja similarity index 93% rename from .cursor/rules/database.mdc rename to template/{% if include_cursor_rules %}.cursor{% endif %}/rules/database.mdc.jinja index c0c6ba9..1a9238d 100644 --- a/.cursor/rules/database.mdc +++ b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/database.mdc.jinja @@ -1,5 +1,6 @@ --- description: Database interaction patterns and best practices +globs: ["**/db/**", "**/database/**", "**/models/**", "**/repositories/**"] alwaysApply: false --- diff --git a/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/development.mdc.jinja b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/development.mdc.jinja new file mode 100644 index 0000000..7fa0f11 --- /dev/null +++ b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/development.mdc.jinja @@ -0,0 +1,61 @@ +--- +description: Development workflow, commands, and tooling for the project +alwaysApply: true +--- + +# Development Workflow + +## Package Management +- Use `uv` for all dependency management and Python environment operations +- Never use `pip` directly; always use `uv` commands +- Sync dependencies: `uv sync --all-extras` + +## Task Runner +- Use `poe` (poethepoet) for running project tasks +- All poe tasks are defined in `scripts/app.toml` +- Run tasks with: `uv run poe ` + +## Available Commands + +### Dependency Management +```bash +uv run poe sync # Sync all dependencies +uv run poe install-hooks # Install pre-commit hooks +``` + +### Code Quality +```bash +uv run poe format # Format code with ruff +uv run poe lint # Lint and auto-fix with ruff +uv run poe check # Run type checker (mypy) +uv run poe test # Run tests with pytest +``` + +### Combined Workflows +```bash +uv run poe flc # Format → Lint → Check (recommended before commits) +uv run poe flct # Format → Lint → Check → Test (full validation) +``` + +## Code Quality Tools + +### Ruff +- Line length: 88 characters (Black default) +- Auto-fix enabled by default +- Comprehensive linting ruleset (`select = ["ALL"]`) +- Specific ignores: ANN (type hints), D (docstrings), COM812, ISC001 + +### MyPy +- Strict mode enabled +- Handles all type checking (not ruff) + +### Pytest +- Code coverage enabled by default +- Reports show missing coverage +- Fails if tests don't cover changes + +## Workflow Best Practices +- Run `uv run poe flc` before committing changes +- Run `uv run poe flct` for complete validation +- Use pre-commit hooks for automated checks +- Address linter errors immediately (don't ignore or suppress) diff --git a/.cursor/rules/python.mdc b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/python.mdc.jinja similarity index 85% rename from .cursor/rules/python.mdc rename to template/{% if include_cursor_rules %}.cursor{% endif %}/rules/python.mdc.jinja index fdeba47..1196c8e 100644 --- a/.cursor/rules/python.mdc +++ b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/python.mdc.jinja @@ -1,14 +1,14 @@ --- -description: Python 3.12+ best practices and modern syntax guidelines +description: Python {{ python_version }}+ best practices and modern syntax guidelines alwaysApply: true --- # Python Version -- Target Python >= 3.12 -- Use only features and syntax compatible with Python 3.12+ -- Leverage modern Python 3.12+ features when appropriate +- Target Python >= {{ python_version }} +- Use only features and syntax compatible with Python {{ python_version }}+ +- Leverage modern Python {{ python_version }}+ features when appropriate -# Python 3.12+ Best Practices +# Python {{ python_version }}+ Best Practices ## Type Hints - Use modern type hint syntax with PEP 604 union types: `str | None` instead of `Optional[str]` diff --git a/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/testing.mdc.jinja b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/testing.mdc.jinja new file mode 100644 index 0000000..e25bc40 --- /dev/null +++ b/template/{% if include_cursor_rules %}.cursor{% endif %}/rules/testing.mdc.jinja @@ -0,0 +1,74 @@ +--- +description: Testing standards and best practices using pytest +globs: ["tests/**/*.py", "**/test_*.py"] +alwaysApply: false +--- + +# Testing Guidelines + +## Test Framework +- Use `pytest` as the testing framework (not `unittest`) +- Run tests with: `uv run poe test` +- Tests are located in the `tests/` directory + +## Test Structure + +### Fixtures +- **Use fixtures when possible** for test setup and teardown +- Prefer fixtures over setup/teardown methods +- Use fixture scopes appropriately (`function`, `class`, `module`, `session`) +- Name fixtures descriptively based on what they provide +- **Use `conftest.py` for shared fixtures**: + - Place global fixtures in `tests/conftest.py` for all tests + - Use nested `conftest.py` files in subdirectories for package-specific fixtures + - Fixtures in `conftest.py` are automatically discovered by pytest + +### Test Organization +- One test file per module: `test_.py` +- **Avoid large test files with independent content** + - When testing different things, create separate files unless there's a reason to keep them together + - Split tests by functionality, feature, or component + - Keep test files focused and manageable in size +- Group related tests in classes when it improves organization +- Use descriptive test names that explain what is being tested +- Follow the Arrange-Act-Assert (AAA) pattern + +### Test Best Practices +- Keep tests focused and test one thing at a time +- Use parametrize for testing multiple scenarios +- Mock external dependencies and I/O operations +- Ensure tests are independent and can run in any order +- Write tests that are fast and deterministic + +## Example Patterns + +```python +import pytest + + +@pytest.fixture +def sample_config(): + """Fixture providing test configuration.""" + return {"key": "value"} + + +def test_feature_with_fixture(sample_config): + """Test using a fixture.""" + assert sample_config["key"] == "value" + + +@pytest.mark.parametrize("input,expected", [ + (1, 2), + (2, 4), + (3, 6), +]) +def test_parametrized(input, expected): + """Test multiple scenarios with parametrize.""" + assert input * 2 == expected +``` + +## Coverage +- Code coverage is enabled by default +- Aim for meaningful coverage, not just high percentages +- Focus on testing critical paths and edge cases +- Coverage reports show missing lines to guide test writing diff --git a/Dockerfile b/template/{% if include_docker %}Dockerfile{% endif %}.jinja similarity index 85% rename from Dockerfile rename to template/{% if include_docker %}Dockerfile{% endif %}.jinja index 87ec6d3..79635db 100644 --- a/Dockerfile +++ b/template/{% if include_docker %}Dockerfile{% endif %}.jinja @@ -1,6 +1,6 @@ # Example taken from https://github.com/astral-sh/uv-docker-example/blob/main/multistage.Dockerfile -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +FROM ghcr.io/astral-sh/uv:python{{ python_version }}-bookworm-slim AS builder # Bytecode compilation, copy from the cache instead of linking, since it is # a mounted volume @@ -16,7 +16,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev --no-editable --all-extras -ADD python_template /app/python_template +ADD {{ project_slug }} /app/{{ project_slug }} ADD pyproject.toml /app/pyproject.toml # Copy the lock file to make sure the Docker environment has the same @@ -29,7 +29,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \ # Then, use a final image without uv # It is important to use the image that matches the builder, as the path to the # Python executable must be the same -FROM python:3.12-slim-bookworm +FROM python:{{ python_version }}-slim-bookworm WORKDIR /app @@ -39,4 +39,4 @@ COPY --from=builder --chown=app:app /app /app # Place executables in the environment at the front of the path ENV PATH="/app/.venv/bin:$PATH" -CMD ["python", "-m", "python_template"] +CMD ["python", "-m", "{{ project_slug }}"] diff --git a/.github/actions/validation/action.yaml b/template/{% if include_github_actions %}.github{% endif %}/actions/validation/action.yaml similarity index 100% rename from .github/actions/validation/action.yaml rename to template/{% if include_github_actions %}.github{% endif %}/actions/validation/action.yaml diff --git a/template/{% if include_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja b/template/{% if include_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja new file mode 100644 index 0000000..03cff26 --- /dev/null +++ b/template/{% if include_github_actions %}.github{% endif %}/workflows/ci.yaml.jinja @@ -0,0 +1,25 @@ +name: CI checks + +on: + pull_request: + +defaults: + run: + shell: bash + +jobs: + format-lint-test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["{{ python_version }}"] + + steps: + - name: 🛎️ Checkout + uses: actions/checkout@v4 + + - name: ✅ validate code + env: + PY_VERSION: {% raw %}${{ matrix.python-version }}{% endraw %} + + uses: ./.github/actions/validation diff --git a/template/{% if license != 'None' %}LICENSE{% endif %}.jinja b/template/{% if license != 'None' %}LICENSE{% endif %}.jinja new file mode 100644 index 0000000..78fb1dc --- /dev/null +++ b/template/{% if license != 'None' %}LICENSE{% endif %}.jinja @@ -0,0 +1,88 @@ +{% if license == 'MIT' -%} +MIT License + +Copyright (c) {{ _current_year }} {{ author_name }} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +{% elif license == 'Apache-2.0' -%} +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +Copyright {{ _current_year }} {{ author_name }} + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +{% elif license == 'GPL-3.0' -%} +GNU GENERAL PUBLIC LICENSE +Version 3, 29 June 2007 + +Copyright (C) {{ _current_year }} {{ author_name }} + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +{% elif license == 'BSD-3-Clause' -%} +BSD 3-Clause License + +Copyright (c) {{ _current_year }}, {{ author_name }} + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +{% endif -%} diff --git a/template/{{project_slug}}/__init__.py b/template/{{project_slug}}/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python_template/main.py b/template/{{project_slug}}/main.py.jinja similarity index 100% rename from python_template/main.py rename to template/{{project_slug}}/main.py.jinja diff --git a/tests/test_template.py b/tests/test_template.py new file mode 100644 index 0000000..c6fc53f --- /dev/null +++ b/tests/test_template.py @@ -0,0 +1,356 @@ +"""Tests for the Copier template generation.""" + +import subprocess +from pathlib import Path + +import pytest +from copier import run_copy + + +@pytest.fixture +def template_path() -> Path: + """Return the path to the template root.""" + return Path(__file__).parent.parent + + +@pytest.fixture +def default_answers() -> dict[str, str | bool]: + """Return default answers for template questions.""" + return { + "project_slug": "test_project", + "description": "A test project", + "author_name": "Test Author", + "author_email": "test@example.com", + "github_username": "testuser", + "python_version": "3.12", + "license": "MIT", + "include_docker": True, + "include_github_actions": True, + "include_cursor_rules": False, + } + + +def test_template_generates_successfully( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that the template generates a valid project structure.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + # Check core files exist + assert (tmp_path / "pyproject.toml").exists() + assert (tmp_path / "README.md").exists() + assert (tmp_path / ".gitignore").exists() + assert (tmp_path / ".pre-commit-config.yaml").exists() + assert (tmp_path / "scripts" / "app.toml").exists() + + # Check project structure + assert (tmp_path / "test_project").is_dir() + assert (tmp_path / "test_project" / "__init__.py").exists() + assert (tmp_path / "test_project" / "main.py").exists() + assert (tmp_path / "tests").is_dir() + assert (tmp_path / "tests" / "test_core.py").exists() + + +def test_template_generates_correct_project_name( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that project name is correctly templated.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + pyproject = (tmp_path / "pyproject.toml").read_text() + assert 'name = "test_project"' in pyproject + + readme = (tmp_path / "README.md").read_text() + # _project_name is derived from project_slug: "test_project" -> "Test Project" + assert "# Test Project" in readme + + +def test_template_generates_project_urls( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that project URLs are generated correctly.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + pyproject = (tmp_path / "pyproject.toml").read_text() + assert "[project.urls]" in pyproject + assert 'Homepage = "https://github.com/testuser/test_project"' in pyproject + assert 'Repository = "https://github.com/testuser/test_project"' in pyproject + assert 'Issues = "https://github.com/testuser/test_project/issues"' in pyproject + + +def test_template_generates_docker_when_enabled( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that Docker files are generated when include_docker is True.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + assert (tmp_path / "Dockerfile").exists() + dockerfile = (tmp_path / "Dockerfile").read_text() + assert "test_project" in dockerfile + + +def test_template_skips_docker_when_disabled( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that Docker files are not generated when include_docker is False.""" + answers = {**default_answers, "include_docker": False} + run_copy( + str(template_path), + tmp_path, + data=answers, + unsafe=True, + ) + + assert not (tmp_path / "Dockerfile").exists() + + +def test_template_generates_github_actions_when_enabled( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that GitHub Actions are generated when include_github_actions is True.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + assert (tmp_path / ".github" / "workflows" / "ci.yaml").exists() + assert (tmp_path / ".github" / "actions" / "validation" / "action.yaml").exists() + + +def test_template_skips_github_actions_when_disabled( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that GitHub Actions are not generated when disabled.""" + answers = {**default_answers, "include_github_actions": False} + run_copy( + str(template_path), + tmp_path, + data=answers, + unsafe=True, + ) + + assert not (tmp_path / ".github").exists() + + +def test_template_generates_cursor_rules_when_enabled( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that Cursor rules are generated when include_cursor_rules is True.""" + answers = {**default_answers, "include_cursor_rules": True} + run_copy( + str(template_path), + tmp_path, + data=answers, + unsafe=True, + ) + + assert (tmp_path / ".cursor" / "rules").is_dir() + assert (tmp_path / ".cursor" / "rules" / "python.mdc").exists() + assert (tmp_path / ".cursor" / "rules" / "development.mdc").exists() + assert (tmp_path / ".cursor" / "rules" / "testing.mdc").exists() + assert (tmp_path / ".cursor" / "rules" / "database.mdc").exists() + + # Verify Python version is templated + python_rules = (tmp_path / ".cursor" / "rules" / "python.mdc").read_text() + assert "Python 3.12" in python_rules + + +def test_template_skips_cursor_rules_when_disabled( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that Cursor rules are not generated when disabled.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + assert not (tmp_path / ".cursor").exists() + + +def test_template_generates_license_mit( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that MIT license is generated correctly.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + license_file = tmp_path / "LICENSE" + assert license_file.exists() + license_content = license_file.read_text() + assert "MIT License" in license_content + assert "Test Author" in license_content + + +def test_template_skips_license_when_none( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that no license file is generated when license is None.""" + answers = {**default_answers, "license": "None"} + run_copy( + str(template_path), + tmp_path, + data=answers, + unsafe=True, + ) + + assert not (tmp_path / "LICENSE").exists() + + # Also verify license field is not in pyproject.toml + pyproject = (tmp_path / "pyproject.toml").read_text() + assert "license" not in pyproject.lower() or "license =" not in pyproject + + +def test_template_generates_copier_answers_file( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that .copier-answers.yml is generated.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + answers_file = tmp_path / ".copier-answers.yml" + assert answers_file.exists() + content = answers_file.read_text() + assert "project_slug: test_project" in content + + +def test_generated_project_passes_validation( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that the generated project passes linting, formatting, and type checking.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + # Initialize uv and sync dependencies + result = subprocess.run( + ["uv", "sync", "--all-extras"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"uv sync failed: {result.stderr}" + + # Run format check + result = subprocess.run( + ["uv", "run", "ruff", "format", "--check", str(tmp_path)], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"ruff format check failed: {result.stdout}" + + # Run lint check + result = subprocess.run( + ["uv", "run", "ruff", "check", str(tmp_path)], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"ruff lint failed: {result.stdout}" + + # Run type check with mypy + result = subprocess.run( + ["uv", "run", "mypy", str(tmp_path)], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"mypy failed: {result.stdout}\n{result.stderr}" + + +def test_generated_project_tests_pass( + template_path: Path, + default_answers: dict[str, str | bool], + tmp_path: Path, +) -> None: + """Test that the generated project's tests pass.""" + run_copy( + str(template_path), + tmp_path, + data=default_answers, + unsafe=True, + ) + + # Sync dependencies + subprocess.run( + ["uv", "sync", "--all-extras"], + cwd=tmp_path, + capture_output=True, + text=True, + check=True, + ) + + # Run pytest + result = subprocess.run( + ["uv", "run", "pytest", str(tmp_path / "tests"), "-v"], + cwd=tmp_path, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, f"pytest failed: {result.stdout}\n{result.stderr}" diff --git a/uv.lock b/uv.lock index f2d733d..f769a44 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 1 requires-python = ">=3.12" +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + [[package]] name = "cfgv" version = "3.5.0" @@ -20,6 +29,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "copier" +version = "9.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "dunamai" }, + { name = "funcy" }, + { name = "jinja2" }, + { name = "jinja2-ansible-filters" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "plumbum" }, + { name = "pydantic" }, + { name = "pygments" }, + { name = "pyyaml" }, + { name = "questionary" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/61/226642b1efad2a39008ee5b913cd82a6f22a564b652e8f0645488a27a2e2/copier-9.11.0.tar.gz", hash = "sha256:e73d6989fa140b621a5c571984c46122704086a9caa84a6e07699a5234d297ab", size = 592030 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/ee/657b24e9b2406f572db273e943237c39c86da7d06ac7bc0686cacea1f59d/copier-9.11.0-py3-none-any.whl", hash = "sha256:628adac090f7b333bb64bf5cab03456b99971a77e4d5b2b979e30b8451cbda9d", size = 56432 }, +] + [[package]] name = "coverage" version = "7.13.0" @@ -103,6 +136,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047 }, ] +[[package]] +name = "dunamai" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/2f/194d9a34c4d831c6563d2d990720850f0baef9ab60cb4ad8ae0eff6acd34/dunamai-1.25.0.tar.gz", hash = "sha256:a7f8360ea286d3dbaf0b6a1473f9253280ac93d619836ad4514facb70c0719d1", size = 46155 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/41/04e2a649058b0713b00d6c9bd22da35618bb157289e05d068e51fddf8d7e/dunamai-1.25.0-py3-none-any.whl", hash = "sha256:7f9dc687dd3256e613b6cc978d9daabfd2bb5deb8adc541fc135ee423ffa98ab", size = 27022 }, +] + [[package]] name = "filelock" version = "3.20.0" @@ -112,6 +157,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, ] +[[package]] +name = "funcy" +version = "2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/c6081521ff70afdff55cd9512b2220bbf4fa88804dae51d1b57b4b58ef32/funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb", size = 537931 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891 }, +] + [[package]] name = "identify" version = "2.6.15" @@ -130,6 +184,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jinja2-ansible-filters" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/27/fa186af4b246eb869ffca8ffa42d92b05abaec08c99329e74d88b2c46ec7/jinja2-ansible-filters-1.3.2.tar.gz", hash = "sha256:07c10cf44d7073f4f01102ca12d9a2dc31b41d47e4c61ed92ef6a6d2669b356b", size = 16945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/b9/313e8f2f2e9517ae050a692ae7b3e4b3f17cc5e6dfea0db51fe14e586580/jinja2_ansible_filters-1.3.2-py3-none-any.whl", hash = "sha256:e1082f5564917649c76fed239117820610516ec10f87735d0338688800a55b34", size = 18975 }, +] + [[package]] name = "librt" version = "0.7.3" @@ -182,6 +261,69 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/0c/6605b6199de8178afe7efc77ca1d8e6db00453bc1d3349d27605c0f42104/librt-0.7.3-cp314-cp314t-win_arm64.whl", hash = "sha256:a9f9b661f82693eb56beb0605156c7fca57f535704ab91837405913417d6990b", size = 45647 }, ] +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + [[package]] name = "mypy" version = "1.19.0" @@ -278,6 +420,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] +[[package]] +name = "plumbum" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/c8/11a5f792704b70f071a3dbc329105a98e9cc8d25daaf09f733c44eb0ef8e/plumbum-1.10.0.tar.gz", hash = "sha256:f8cbf0ecec0b73ff4e349398b65112a9e3f9300e7dc019001217dcc148d5c97c", size = 320039 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/ad/45312df6b63ba64ea35b8d8f5f0c577aac16e6b416eafe8e1cb34e03f9a7/plumbum-1.10.0-py3-none-any.whl", hash = "sha256:9583d737ac901c474d99d030e4d5eec4c4e6d2d7417b1cf49728cf3be34f6dc8", size = 127383 }, +] + [[package]] name = "poethepoet" version = "0.38.0" @@ -307,6 +461,104 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431 }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -347,12 +599,13 @@ wheels = [ ] [[package]] -name = "python-template" +name = "python-copier-template" version = "0.0.1" -source = { editable = "." } +source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "copier" }, { name = "mypy" }, { name = "poethepoet" }, { name = "pre-commit" }, @@ -365,6 +618,7 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "copier", specifier = ">=9.0.0" }, { name = "mypy", specifier = ">=1.15.0" }, { name = "poethepoet", specifier = ">=0.35.0" }, { name = "pre-commit", specifier = ">=4.1.0" }, @@ -373,6 +627,22 @@ dev = [ { name = "ruff", specifier = ">=0.9.5" }, ] +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -419,6 +689,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, ] +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753 }, +] + [[package]] name = "ruff" version = "0.14.9" @@ -454,6 +736,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + [[package]] name = "virtualenv" version = "20.35.4" @@ -467,3 +761,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846 wheels = [ { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095 }, ] + +[[package]] +name = "wcwidth" +version = "0.2.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286 }, +]