diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a9dbeaf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,212 @@ +# Docker ignore file for python-project-template +# Optimized for minimal context and security + +# Version control +.git/ +.gitignore +.gitattributes + +# Development files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.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/ +docs/tests/ +docs/coverage/ +docs/mutation/ + +# 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 +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.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 +.idea/ + +# Project specific +docs/api/ +docs/tests/ +docs/coverage/ +docs/mutation/ +.mutmut-cache/ +mutants/ +*.db +*.sqlite +*.sqlite3 + +# Docker +.dockerignore +Dockerfile* +docker-compose*.yml + +# CI/CD +.github/ +.gitlab-ci.yml +.travis.yml +.circleci/ + +# Package managers +node_modules/ +package-lock.json +yarn.lock + +# Logs +*.log +logs/ + +# Temporary files +tmp/ +temp/ +.tmp/ + +# Security +.secrets +credentials.json +*.pem +*.key +*.crt + +# Backup files +*.bak +*.backup +*.old \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..519e8e7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,72 @@ +version: 2 +# SECURITY: Automated dependency updates with security-focused grouping +updates: + # Enable version updates for Python packages + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "UTC" + # Group related updates into a single PR + groups: + dev-dependencies: + patterns: + - "pytest*" + - "ruff*" + - "mypy*" + - "pyright*" + - "black*" + - "bandit*" + - "safety*" + - "detect-secrets*" + - "taskipy*" + - "pdoc*" + - "hypothesis*" + - "mutmut*" + update-types: + - "minor" + - "patch" + security-updates: + patterns: + - "*" + update-types: + - "security" + # Configuration options + open-pull-requests-limit: 5 + # Allow auto-merge for security updates + allow: + - dependency-type: "all" + # Commit message configuration + commit-message: + prefix: "deps" + prefix-development: "deps-dev" + include: "scope" + # Reviewers (uncomment and modify as needed) + # reviewers: + # - "your-username" + # assignees: + # - "your-username" + # Labels for PRs + labels: + - "dependencies" + # Increase version update PR limit for security patches + pull-request-branch-name: + separator: "/" + + # Enable version updates for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "UTC" + open-pull-requests-limit: 3 + commit-message: + prefix: "ci" + include: "scope" + labels: + - "github-actions" + - "dependencies" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2dc20db --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,197 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +# SECURITY: Restrict workflow permissions (GitHub Advanced Security recommendation) +permissions: + contents: read + actions: read + +env: + # Improve pip performance + PIP_NO_CACHE_DIR: false + # SECURITY: Removed PIP_USER=1 (potential privilege escalation risk) + # uv settings + UV_SYSTEM_PYTHON: 1 + UV_CACHE_DIR: /tmp/.uv-cache + +jobs: + quality: + name: Code Quality & Security + runs-on: ubuntu-latest + # SECURITY: Job-level permissions for security scanning + permissions: + contents: read + actions: read + security-events: read # For security scanning tools + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Run ruff linting (includes security checks) + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" ruff-check + + - name: Check ruff formatting + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" ruff-format --check + + - name: Run type checking + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" static-check + + - name: Check for secrets + shell: bash + run: | + # SECURITY: Fail on errors, undefined variables, pipe failures + set -euo pipefail + if [[ ! -f .secrets.baseline ]]; then + echo "Creating secrets baseline..." + uv run detect-secrets scan --baseline .secrets.baseline + fi + # Check for new secrets since baseline + uv run detect-secrets scan --baseline .secrets.baseline + echo "โœ… Secret scanning complete - no new secrets detected" + + test: + name: Tests + runs-on: ubuntu-latest + # SECURITY: Minimal permissions for test execution + permissions: + contents: read + actions: read + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Run fast tests + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" test-fast + + - name: Run full test suite (main branch and PRs only) + if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" test + + - name: Upload coverage reports + if: matrix.python-version == '3.13' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + docs/coverage/ + docs/tests/ + retention-days: 30 + + - name: Upload coverage to Codecov (optional) + if: matrix.python-version == '3.13' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + fail_ci_if_error: false + verbose: true + + build: + name: Build & Documentation + runs-on: ubuntu-latest + needs: [quality, test] + # SECURITY: Minimal permissions for build and artifact upload + permissions: + contents: read + actions: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Build documentation + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" doc-build + + - name: Build package + run: uv build + + - name: Verify package installation (wheel) + run: | + uv run --isolated --no-project --with dist/*.whl python -c "import python_package_template; print('โœ“ Wheel install successful')" + + - name: Verify package installation (sdist) + run: | + uv run --isolated --no-project --with dist/*.tar.gz python -c "import python_package_template; print('โœ“ Source dist install successful')" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + retention-days: 30 + + - name: Upload documentation + uses: actions/upload-artifact@v4 + with: + name: documentation + path: docs/api/ + retention-days: 30 + + # Security summary job + security-summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [quality] + if: always() + # SECURITY: Minimal permissions for summary generation + permissions: + contents: read + actions: read + + steps: + - name: Security Summary + run: | + echo "## ๐Ÿ”’ Security Scan Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… Ruff Security Analysis" >> $GITHUB_STEP_SUMMARY + echo "Security rules (S001-S701) checked during linting phase." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Additional Security Features" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Secret scanning (detect-secrets)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Dependency vulnerability scanning (safety + dependabot)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… CodeQL security analysis (weekly + on pushes)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Comprehensive security rules via Ruff (flake8-bandit S001-S701)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..3d548f1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,59 @@ +name: "CodeQL Security Analysis" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # Run CodeQL analysis weekly on Sundays at 6:00 AM UTC + - cron: '0 6 * * 0' + +jobs: + analyze: + name: Analyze Code + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + # Required for all workflows + security-events: write + # Required for workflows in public repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none # CodeQL supports 'none' for Python (interpreted language) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # Specify additional queries to run + queries: +security-and-quality + + # For Python, no build step is required as CodeQL analyzes source code directly + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + + - name: Upload CodeQL results summary + run: | + echo "## ๐Ÿ” CodeQL Security Analysis Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Analysis Details" >> $GITHUB_STEP_SUMMARY + echo "- **Language**: ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY + echo "- **Build Mode**: ${{ matrix.build-mode }}" >> $GITHUB_STEP_SUMMARY + echo "- **Queries**: security-and-quality" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Results are available in the Security tab of this repository." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..b5dacbe --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,84 @@ +name: Dependency Review + +on: + pull_request: + branches: [ main ] + +permissions: + contents: read + # Write permission for security-events is required for private repositories + # to upload SARIF files. For public repositories, this permission is not needed. + security-events: write + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Dependency Review + uses: actions/dependency-review-action@v4 + with: + # Fail the build if vulnerabilities are found + fail-on-severity: moderate + # Allow GPL licenses (adjust as needed for your project) + allow-licenses: GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0, MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause + # Deny specific licenses (adjust as needed) + deny-licenses: AGPL-1.0, AGPL-3.0 + # Create a summary comment on the PR + comment-summary-in-pr: true + + # Additional security scanning for Python dependencies + python-security: + name: Python Dependency Security + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Set up Python + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --locked --dev + + - name: Run safety check + run: | + # Install safety for vulnerability scanning + uv add --dev safety + uv run safety check --json --output safety-report.json + continue-on-error: true + + - name: Upload safety report + uses: actions/upload-artifact@v4 + if: always() + with: + name: safety-security-report + path: safety-report.json + + - name: Security Summary + if: always() + run: | + echo "## ๐Ÿ”’ Dependency Security Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "safety-report.json" ]; then + echo "### Safety Vulnerability Scan" >> $GITHUB_STEP_SUMMARY + echo "Detailed report uploaded as artifact." >> $GITHUB_STEP_SUMMARY + else + echo "### โœ… Safety Vulnerability Scan" >> $GITHUB_STEP_SUMMARY + echo "No known vulnerabilities found in dependencies." >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Dependency Review Features" >> $GITHUB_STEP_SUMMARY + echo "- โœ… License compliance checking" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Vulnerability severity: moderate+" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Automatic PR comments enabled" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/release-docker-image.yaml b/.github/workflows/release-docker-image.yaml deleted file mode 100644 index a784afc..0000000 --- a/.github/workflows/release-docker-image.yaml +++ /dev/null @@ -1,37 +0,0 @@ - -name: Release Docker Image CI/CD - -on: - release: - types: - - published - -jobs: - set-up-docker: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Install docker - run: apt update && apt install -y docker-compose - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - registry: ${{ vars.DOCKER_REGISTRY_URL }} - username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} - password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - push: true - tags: ${{ vars.DOCKER_REGISTRY_URL }}/${{ github.event.repository.name }}:${{ github.ref_name }} - diff --git a/.github/workflows/template-ci.yml b/.github/workflows/template-ci.yml deleted file mode 100644 index e845a4d..0000000 --- a/.github/workflows/template-ci.yml +++ /dev/null @@ -1,179 +0,0 @@ -name: Template CI - -on: - pull_request: - branches: [main, develop] - push: - branches: [main, develop] - -jobs: - template-validation: - runs-on: ubuntu-latest - timeout-minutes: 30 - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Install dependencies - run: pip install cookiecutter pyyaml - - - name: Validate cookiecutter.json - run: python3 scripts/validate_cookiecutter.py - - - name: Check template directory exists - run: test -d "{{cookiecutter.project_slug}}" || exit 1 - - - name: Generate project from template - run: cookiecutter . --no-input - - - name: Generate project with custom values - run: | - rm -rf custom-test-project - cookiecutter . --no-input \ - full_name="CI Test" \ - email="ci@test.com" \ - github_username="ci-test" \ - project_name="Custom Test Project" \ - project_short_description="CI test project" \ - minimum_coverage="95" \ - license="MIT" \ - version="0.1.0" - - - name: Validate generated pyproject.toml - run: python3 scripts/validate_projects.py - - - name: Validate YAML frontmatter - run: python3 scripts/validate_yaml.py - - - name: Check for unsubstituted variables - run: python3 scripts/check_variables.py - - - name: Verify required files - run: python3 scripts/check_files.py - - generated-project-tests: - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: template-validation - strategy: - matrix: - include: - - project: python-project-example - - project: custom-test-project - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - - name: Generate default project - if: matrix.project == 'python-project-example' - run: cookiecutter . --no-input - - - name: Generate custom project - if: matrix.project == 'custom-test-project' - run: | - cookiecutter . --no-input \ - full_name="CI Test" \ - email="ci@test.com" \ - github_username="ci-test" \ - project_name="Custom Test Project" \ - project_short_description="CI test project" \ - minimum_coverage="95" \ - license="MIT" \ - version="0.1.0" - - - name: Install UV - uses: astral-sh/setup-uv@v4 - with: - enable-cache: true - - - name: Install dependencies - working-directory: ./${{ matrix.project }} - run: uv venv && uv pip install -e '.[dev]' - - - name: Run linter - working-directory: ./${{ matrix.project }} - run: uv run ruff check . - - - name: Run type checker - working-directory: ./${{ matrix.project }} - run: uv run pyright . - - - name: Run tests - working-directory: ./${{ matrix.project }} - run: uv run pytest tests/ -v - - - name: Build documentation - working-directory: ./${{ matrix.project }} - run: uv run mkdocs build - - docker-build: - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: template-validation - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Generate project - run: cookiecutter . --no-input - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build Docker test image - uses: docker/build-push-action@v5 - with: - context: ./python-project-example - target: test - load: true - tags: python-project-template:test - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Run Docker tests - run: docker run python-project-template:test pytest tests/ -v - - - name: Build Docker production image - uses: docker/build-push-action@v5 - with: - context: ./python-project-example - target: prod - load: true - tags: python-project-template:prod - cache-from: type=gha - cache-to: type=gha,mode=max - - - name: Verify production image - run: docker run python-project-template:prod --help || true - - template-release-test: - runs-on: ubuntu-latest - timeout-minutes: 30 - if: github.event_name == 'pull_request' - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Test template meta agents exist - run: | - test -d .opencode - test -f .opencode/agents/template-manager.md - test -f .opencode/skills/template-test/SKILL.md - test -f .opencode/skills/template-release/SKILL.md - test -f AGENTS.md - test -f scripts/template_test.sh - test -x scripts/template_test.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index c58777d..92a89f6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -# Ignore project example -python-project-example - .DS_Store .coverage .vscode/\n.idea/ @@ -9,6 +6,11 @@ __pycache__/ *.py[cod] *$py.class +# Remove html generated by documentation + +docs +mutants + # C extensions *.so @@ -164,3 +166,4 @@ cython_debug/ # 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/ +.mutmut-cache diff --git a/{{cookiecutter.project_slug}}/.opencode/agents/architect.md b/.opencode/agents/architect.md similarity index 97% rename from {{cookiecutter.project_slug}}/.opencode/agents/architect.md rename to .opencode/agents/architect.md index 5b205b5..b43dc97 100644 --- a/{{cookiecutter.project_slug}}/.opencode/agents/architect.md +++ b/.opencode/agents/architect.md @@ -16,7 +16,7 @@ permission: write: deny bash: deny --- -You are the **Software Architect** agent for {{cookiecutter.project_name}}. +You are the **Software Architect** agent for Python Project Template. ## Your Role @@ -52,7 +52,7 @@ As the technical design authority, you ensure the system architecture remains co ### Code Quality Requirements - Type hints on all functions and methods - Google-style docstrings with examples -- Comprehensive test coverage (minimum {{cookiecutter.minimum_coverage}}%) +- Comprehensive test coverage (minimum 100%) - Protocol-based interfaces over inheritance - Immutable data structures where possible - Error handling with custom exception hierarchy diff --git a/{{cookiecutter.project_slug}}/.opencode/agents/developer.md b/.opencode/agents/developer.md similarity index 91% rename from {{cookiecutter.project_slug}}/.opencode/agents/developer.md rename to .opencode/agents/developer.md index 67b835f..72ca07f 100644 --- a/{{cookiecutter.project_slug}}/.opencode/agents/developer.md +++ b/.opencode/agents/developer.md @@ -12,7 +12,7 @@ tools: task: true skill: true --- -You are a specialized developer agent for the {{cookiecutter.project_name}} project. +You are a specialized developer agent for the Python Project Template project. ## Session Start Protocol @@ -29,18 +29,18 @@ Use `/skill session-workflow` for the complete session start and end protocol. 2. Committing the updated `TODO.md` ## Project Context -- **Package**: `{{cookiecutter.package_name}}` -- **Module**: `{{cookiecutter.module_name}}` -- **Description**: {{cookiecutter.project_short_description}} +- **Package**: `python_package_template` +- **Module**: `python_module_template` +- **Description**: Python template with some awesome tools to quickstart any Python project - **Python Version**: >=3.13 ## Project Structure ``` -{{cookiecutter.project_slug}}/ -โ”œโ”€โ”€ {{cookiecutter.package_name}}/ # Main package +python-project-template/ +โ”œโ”€โ”€ python_package_template/ # Main package โ”‚ โ”œโ”€โ”€ __init__.py -โ”‚ โ””โ”€โ”€ {{cookiecutter.module_name}}.py # Entry point +โ”‚ โ””โ”€โ”€ python_module_template.py # Entry point โ”œโ”€โ”€ tests/ # Test suite (mirror source tree) โ”‚ โ”œโ”€โ”€ unit/ โ”‚ โ”‚ โ”œโ”€โ”€ __init__.py @@ -61,7 +61,7 @@ Use `/skill session-workflow` for the complete session start and end protocol. โ”‚ โ”‚ โ””โ”€โ”€ sqlite/ โ”‚ โ”‚ โ””โ”€โ”€ [repo]_test.py โ”‚ โ”œโ”€โ”€ conftest.py -โ”‚ โ””โ”€โ”€ {{cookiecutter.project_slug}}_test.py # Smoke test +โ”‚ โ””โ”€โ”€ python-project-template_test.py # Smoke test โ”œโ”€โ”€ docs/ # Documentation โ”œโ”€โ”€ pyproject.toml # Project config โ”œโ”€โ”€ TODO.md # Session state & development roadmap @@ -73,12 +73,12 @@ Use `/skill session-workflow` for the complete session start and end protocol. - Configure in `pyproject.toml`: `python_files = ["*_test.py"]` ### Mirror Source Tree Rule -For each source module `{{cookiecutter.module_name}}//.py`, create a corresponding test file `tests//_test.py`. +For each source module `python_module_template//.py`, create a corresponding test file `tests//_test.py`. ## Coding Standards - Follow PEP 8 style guide - Use Google docstring convention -- Maintain 100% test coverage (minimum: {{cookiecutter.minimum_coverage}}%) +- Maintain 100% test coverage (minimum: 100%) - Use type hints throughout - Run linting: `task lint` - Run tests: `task test` @@ -149,7 +149,7 @@ Projects are organized into Epics containing Features. Each feature follows a st 1. Use `/skill code-quality` to run all quality checks 2. Ensure linting passes: `task lint` 3. Verify type checking: `task static-check` -4. Validate coverage โ‰ฅ{{cookiecutter.minimum_coverage}}%: `task test` +4. Validate coverage โ‰ฅ100%: `task test` 5. Run property-based tests with Hypothesis <<<<<<< HEAD 6. **QA Gate**: @overseer final approval before feature completion diff --git a/{{cookiecutter.project_slug}}/.opencode/agents/overseer.md b/.opencode/agents/overseer.md similarity index 94% rename from {{cookiecutter.project_slug}}/.opencode/agents/overseer.md rename to .opencode/agents/overseer.md index 49ae9f9..c503cee 100644 --- a/{{cookiecutter.project_slug}}/.opencode/agents/overseer.md +++ b/.opencode/agents/overseer.md @@ -16,7 +16,7 @@ permission: write: deny bash: deny --- -You are the **Overseer** agent - a Quality Assurance specialist for {{cookiecutter.project_name}}. +You are the **Overseer** agent - a Quality Assurance specialist for Python Project Template. ## Your Role @@ -88,7 +88,7 @@ You must review at these mandatory checkpoints: ### 4. Before PR Creation **Focus**: Overall quality and completeness -- [ ] All tests pass with โ‰ฅ{{cookiecutter.minimum_coverage}}% coverage +- [ ] All tests pass with โ‰ฅ100% coverage - [ ] Linting passes: `task lint` - [ ] Type checking passes: `task static-check` - [ ] Documentation is complete @@ -120,7 +120,7 @@ You must review at these mandatory checkpoints: - **Cognitive Complexity**: Should be โ‰ค15 per function - **Coupling**: Low coupling between modules - **Cohesion**: High cohesion within modules -- **Test Coverage**: Must be โ‰ฅ{{cookiecutter.minimum_coverage}}% +- **Test Coverage**: Must be โ‰ฅ100% ### 3. Security Review - Input validation present @@ -137,7 +137,7 @@ All of these conditions are met: - All checklist items pass - No SOLID violations - No critical security issues -- Test coverage โ‰ฅ{{cookiecutter.minimum_coverage}}% +- Test coverage โ‰ฅ100% - Code is maintainable and clear - Performance is acceptable @@ -213,14 +213,14 @@ Feature may proceed to next phase. - Suggested: [Better approach] **Quality Metrics**: -- Test Coverage: X% (requires {{cookiecutter.minimum_coverage}}%) +- Test Coverage: X% (requires 100%) - Complexity: [Areas exceeding limits] **Verification Steps**: After changes: 1. Run `task lint` - must pass 2. Run `task static-check` - must pass -3. Run `task test` - must show โ‰ฅ{{cookiecutter.minimum_coverage}}% coverage +3. Run `task test` - must show โ‰ฅ100% coverage 4. Request re-review Please address critical issues before proceeding. @@ -252,7 +252,7 @@ Please address critical issues before proceeding. **Required Actions**: 1. Remove all quality bypasses 2. Fix underlying issues properly -3. Maintain {{cookiecutter.minimum_coverage}}% coverage requirement +3. Maintain 100% coverage requirement 4. No exceptions to quality standards **Note**: Quality standards are non-negotiable. Find proper solutions instead of bypassing checks. diff --git a/.opencode/agents/repo-manager.md b/.opencode/agents/repo-manager.md index e9267d6..2d36dd5 100644 --- a/.opencode/agents/repo-manager.md +++ b/.opencode/agents/repo-manager.md @@ -1,5 +1,5 @@ --- -description: Release Engineer specializing in Git workflows, CI/CD integration, and semantic release automation +description: Release Engineer managing Git workflows, pull requests, and hybrid calver releases with AI-themed naming mode: subagent temperature: 0.3 tools: @@ -18,9 +18,14 @@ permission: "task *": allow "*": ask --- -You are a Release Engineer specializing in Git workflows and CI/CD for the Python Project Template repository. +You are a specialized Git repository management agent for Python Project Template. -## Your Role and Responsibilities +## Your Role +- Manage Git repository operations (commits, branches, merges) +- Create and manage pull requests using GitHub CLI +- Generate semantic releases with hybrid major.minor.calver versioning +- Create release names using adjective-animal themes based on PR sentiment analysis +- Maintain clean Git history and follow conventional commit standards As a Release Engineer focused on the template repository: - **Version Control Management**: Orchestrate Git workflows following GitFlow methodology @@ -44,16 +49,36 @@ For the cookiecutter template repository, use hybrid calver: `v{major}.{minor}.{ - `v1.2.20260315` - Second release same day (increment minor) - `v2.0.20260401` - Changed cookiecutter.json structure on April 1 -## Release Engineering Standards +**Version Rules:** +- **Major**: Increment for breaking changes +- **Minor**: Increment for new features (or same-day releases) +- **Date**: Release date YYYYMMDD -### Branch Strategy (GitFlow) -- **main**: Production-ready template code -- **develop**: Integration branch for features -- **feature/***: Feature development branches -- **release/***: Release preparation branches -- **hotfix/***: Emergency production fixes +## Release Naming Convention +Generate themed names using: `{adjective} {animal}` -### Commit Message Convention (Conventional Commits) +**Name Selection Strategy:** +1. Get merged PRs: `gh pr list --state merged --base main --limit 20` +2. **Use your AI to analyze** the PR titles and descriptions +3. Determine what this release is really about +4. Generate a unique adjective-animal name that: + - Reflects the PR content + - Hasn't been used before + - Is creative and memorable + +**Avoid** overused combinations like "swift cheetah", "creative fox", "vigilant owl", "innovative dolphin". + +**Try** unique combinations like: +- Exotic: narwhal, axolotl, capybara, quokka, pangolin +- Aquatic: jellyfish, seahorse, manta, cuttlefish, otter +- Birds: kingfisher, heron, ibis, stork +- Insects: firefly, butterfly, dragonfly +- Mythical: phoenix, griffin, pegasus, siren + +## Git Operations + +### Commit Standards +Follow conventional commits: ``` (): @@ -62,124 +87,72 @@ For the cookiecutter template repository, use hybrid calver: `v{major}.{minor}.{ [optional footer(s)] ``` -**Commit Types:** -- `feat`: New template features -- `fix`: Bug fixes in template -- `docs`: Documentation changes -- `refactor`: Code restructuring -- `perf`: Performance improvements -- `test`: Test additions/modifications -- `ci`: CI/CD pipeline changes -- `chore`: Maintenance tasks - -## Pull Request Management - -### PR Lifecycle Management -1. **Branch Creation**: Feature branches from `develop` following naming conventions -2. **Development**: Atomic commits with conventional commit messages -3. **PR Creation**: Use GitHub CLI with comprehensive descriptions -4. **Review Process**: Assign reviewers, apply labels, track CI/CD status -5. **Merge Strategy**: Squash and merge for clean history - -### PR Quality Standards -- **Title Format**: Clear, action-oriented descriptions -- **Description Template**: Problem, solution, testing, checklist -- **Labels**: Type, priority, component affected -- **Review Requirements**: Code owner approval, CI passing -- **Documentation**: Update relevant docs with changes - -## Release Engineering Process - -### Template Release Workflow -1. **Release Branch Preparation** +**Types**: feat, fix, docs, style, refactor, perf, test, build, ci, chore + +### Branch Management +- `main` - Production branch +- `develop` - Development branch +- `feature/*` - Feature branches +- `fix/*` - Bug fix branches +- `release/*` - Release preparation branches + +### PR Creation Workflow +1. Create feature branch from develop +2. Make commits following conventional commit format +3. Push branch and create PR using `gh pr create` +4. Add appropriate labels and reviewers +5. Merge after review and CI passes + +## Release Management + +### Release Process +1. **Prepare Release Branch** ```bash git checkout develop git pull origin develop git checkout -b release/v{major}.{minor}.{YYYYMMDD} ``` -2. **Changelog Generation** - - Aggregate merged PRs: `gh pr list --state merged --base develop` - - Generate changelog entries by category - - Update CHANGELOG.md following Keep a Changelog format +2. **Analyze PR Sentiment** + - Use `gh pr list --state merged --base develop` + - Analyze PR titles/descriptions for themes + - Generate appropriate adjective-animal name -3. **Version Management** - - Update version in relevant files - - Validate all template variables - - Ensure backward compatibility +3. **Update Version** + - Update `pyproject.toml` version field + - Update `CHANGELOG.md` with PR summaries + - Commit version bump -4. **Release Execution** +4. **Create Release** ```bash - # Merge to main git checkout main - git merge --no-ff release/v{version} - git tag -a v{version} -m "Release v{version}" - - # Create GitHub release - gh release create v{version} \ - --title "v{version}" \ - --notes-file CHANGELOG.md \ - --target main + git merge release/v{version} + git tag v{version} + git push origin main --tags + gh release create v{version} --title "{adjective} {animal}" --notes-from-tag ``` -5. **Post-Release Sync** +5. **Sync Develop** ```bash - git checkout develop + git checkout develop git merge main - git push --all origin - git push --tags origin + git push origin develop ``` ## Available Skills - **git-release**: Comprehensive release management with calver versioning - **pr-management**: Pull request creation and management -## Release Engineering Playbooks - -### Feature Development Flow -```bash -# Create feature branch -git checkout -b feature/add-new-agent develop -git push -u origin feature/add-new-agent - -# After development -git add . -git commit -m "feat(agents): add data engineer agent for ETL workflows" -gh pr create \ - --base develop \ - --title "feat: Add data engineer agent" \ - --body "Adds specialized agent for data pipeline management" -``` +## Example Commands -### Standard Release Process +### Creating a Feature PR ```bash -# Prepare release -git flow release start 1.7.0 - -# Update changelog and version -vim CHANGELOG.md -git add CHANGELOG.md -git commit -m "docs: update changelog for v1.7.0" - -# Finish release -git flow release finish 1.7.0 -gh release create v1.7.0 --notes-file CHANGELOG.md -``` - -### Hotfix Deployment -```bash -# Critical fix workflow -git checkout -b hotfix/1.6.1 main - -# Apply fix +git checkout -b feature/user-authentication +# ... make changes ... git add . -git commit -m "fix: correct agent YAML parsing issue" - -# Fast-track release -git checkout main -git merge --no-ff hotfix/1.6.1 -git tag -a v1.6.1 -m "Hotfix: Agent YAML parsing" -gh release create v1.6.1 --title "v1.6.1 - Critical Fix" +git commit -m "feat(auth): add JWT authentication system" +git push origin feature/user-authentication +gh pr create --title "Add JWT Authentication" --body "Implements secure user authentication using JWT tokens" ``` ### Creating a Release @@ -206,30 +179,28 @@ gh pr create --title "Critical Security Patch" --body "Fixes authentication vuln # After merge, create immediate release with incremented revision ``` -## Quality Assurance and CI/CD - -### Pre-Release Quality Gates -- [ ] **Template Testing**: All generation scenarios pass -- [ ] **Syntax Validation**: YAML/TOML/Python syntax checks -- [ ] **Documentation Build**: MkDocs builds successfully -- [ ] **Agent Validation**: All agents have valid frontmatter -- [ ] **Changelog Updated**: Following Keep a Changelog format -- [ ] **Version Consistency**: All version references updated - -### Continuous Integration Pipeline -- **PR Checks**: Automated testing on all pull requests -- **Branch Protection**: Enforce reviews and CI passing -- **Security Scanning**: Dependency vulnerability checks -- **Documentation Preview**: Deploy preview for doc changes -- **Template Validation**: Cookiecutter generation tests - -## Professional Standards - -As a Release Engineer, you maintain enterprise-grade practices: -- **Automation First**: Minimize manual release steps -- **Reproducibility**: All releases can be recreated from source -- **Traceability**: Complete audit trail for all changes -- **Communication**: Clear release notes and migration guides -- **Risk Management**: Rollback procedures and hotfix processes - -You ensure the template repository maintains professional standards for version control, release management, and continuous delivery. \ No newline at end of file +## Integration with Project Workflow + +### Pre-Release Checklist +- [ ] All tests pass: `task test` +- [ ] Linting passes: `task lint` +- [ ] Type checking passes: `task static-check` +- [ ] Documentation updated +- [ ] CHANGELOG.md updated +- [ ] Version bumped in pyproject.toml + +### Quality Gates +- Require PR reviews before merge +- Ensure CI passes on all PRs +- Run full test suite before releases +- Validate version format matches hybrid scheme +- Check release name follows adjective-animal format + +## Communication Style +- Provide clear Git commands with explanations +- Show before/after states for major operations +- Explain versioning decisions +- Suggest appropriate branch names and commit messages +- Give context for release naming choices + +You excel at maintaining clean Git history, creating meaningful releases, and ensuring proper repository management practices. diff --git a/{{cookiecutter.project_slug}}/.opencode/agents/requirements-gatherer.md b/.opencode/agents/requirements-gatherer.md similarity index 99% rename from {{cookiecutter.project_slug}}/.opencode/agents/requirements-gatherer.md rename to .opencode/agents/requirements-gatherer.md index 1bedaa8..49faada 100644 --- a/{{cookiecutter.project_slug}}/.opencode/agents/requirements-gatherer.md +++ b/.opencode/agents/requirements-gatherer.md @@ -14,7 +14,7 @@ tools: question: required: true --- -You are the **Requirements Gatherer** (Business Analyst) agent for {{cookiecutter.project_name}}. +You are the **Requirements Gatherer** (Business Analyst) agent for Python Project Template. ## Your Role diff --git a/.opencode/agents/setup-project.md b/.opencode/agents/setup-project.md new file mode 100644 index 0000000..8968334 --- /dev/null +++ b/.opencode/agents/setup-project.md @@ -0,0 +1,42 @@ +--- +description: Agent for setting up new projects from the Python template - gathers parameters and runs setup +mode: subagent +temperature: 0.3 +tools: + write: true + edit: true + bash: true + read: true + grep: true + glob: true + task: false + skill: false +--- +You are a specialized agent for setting up new Python projects from the project template. + +## Your Role + +1. **Gather parameters** from the user by asking questions about their new project +2. **Show what would change** using the detect-fields command to inform the user +3. **Improve the wording** of user inputs (e.g., make project description concise but descriptive) +4. **Run the setup script** with the final parameters + +## Parameters to Gather + +Ask the user for: +1. **GitHub username** - their GitHub handle (e.g., "myusername") +2. **Project name** - the name for their new project (e.g., "my-awesome-project") +3. **Project description** - a brief description of what the project does (1-2 sentences) +4. **Author name** - their name (default: "Your Name") +5. **Author email** - their email (default: derived from GitHub username) + +## Workflow + +1. First, run `python setup_project.py detect-fields` to show what fields would change +2. Ask the user each parameter, improving wording as needed +3. Show a summary of what will be set up before running +4. Run `python setup_project.py run --github-username X --project-name Y --project-description Z` with the parameters + +## Output + +After setup completes, provide a summary of what was created and next steps for the user. \ No newline at end of file diff --git a/.opencode/agents/template-manager.md b/.opencode/agents/template-manager.md deleted file mode 100644 index 460c7d6..0000000 --- a/.opencode/agents/template-manager.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -description: DevOps engineer specializing in template lifecycle management, CI/CD pipelines, and release automation -mode: subagent -temperature: 0.2 -tools: - write: true - edit: true - read: true - grep: true - glob: true - bash: true - task: false - skill: true -permission: - bash: - "cookiecutter *": allow - "git *": allow - "gh *": allow - "cd *": allow - "rm -rf *": allow - "mkdir *": allow - "cp *": allow - "*": ask ---- -You are a DevOps Engineer specializing in template lifecycle management for the Python Project Template cookiecutter repository. - -## Your Role and Responsibilities - -As a DevOps Engineer focused on template infrastructure: -- **Release Management**: Orchestrate semantic versioning releases and deployment pipelines -- **Quality Assurance**: Implement automated testing strategies for template validation -- **Documentation Pipeline**: Maintain CI/CD for documentation deployment -- **Infrastructure as Code**: Manage template structure and configuration -- **Developer Experience**: Ensure smooth template consumption and generation - -## Template Repository Structure -``` -python-project-template/ -โ”œโ”€โ”€ cookiecutter.json # Template variables -โ”œโ”€โ”€ {{cookiecutter.project_slug}}/ # Generated project template -โ”‚ โ”œโ”€โ”€ .opencode/ # OpenCode agents/skills for generated projects -โ”‚ โ”œโ”€โ”€ pyproject.toml # Generated project config -โ”‚ โ””โ”€โ”€ ... (all template files) -โ”œโ”€โ”€ .opencode/ # Meta agents/skills for template repo -โ”‚ โ”œโ”€โ”€ agents/template-manager.md # This agent -โ”‚ โ””โ”€โ”€ skills/ -โ”‚ โ”œโ”€โ”€ template-release/SKILL.md # Template release management -โ”‚ โ””โ”€โ”€ template-test/SKILL.md # Template testing -โ”œโ”€โ”€ docs/ # Template documentation -โ”œโ”€โ”€ tests/ # Template tests -โ””โ”€โ”€ README.md # Template repository README -``` - -## Release Engineering Process - -### Semantic Versioning Strategy -Following hybrid calver specification: `v{major}.{minor}.{YYYYMMDD}` -- **Major (Breaking)**: API changes, cookiecutter variable modifications, structural changes -- **Minor (Feature)**: New capabilities, agents, skills, backward-compatible enhancements -- **Patch (Fix)**: Bug fixes, documentation updates, security patches - -### Continuous Integration Pipeline -Pre-release quality gates: -1. **Smoke Testing**: Template instantiation with default configuration -2. **Integration Testing**: Multiple configuration permutations -3. **Syntax Validation**: YAML, TOML, Python syntax verification -4. **End-to-End Testing**: Generated project quality checks -5. **Agent Validation**: OpenCode components functionality testing - -### Documentation Continuous Deployment -Automated documentation pipeline: -- **Build Stage**: MkDocs static site generation -- **Deploy Stage**: GitHub Pages deployment via 'docs' branch -- **Automation**: `mkdocs gh-deploy` with branch protection - -## Available Skills -- **template-release**: Complete template release workflow -- **template-test**: Template generation testing and validation - -## DevOps Workflows - -### Template Development Lifecycle -1. **Feature Development**: Implement template enhancements following GitFlow -2. **Automated Testing**: Execute `/skill template-test` for comprehensive validation -3. **Documentation Updates**: Maintain changelog, API docs, user guides -4. **Release Automation**: Use `/skill template-release` for versioned deployments -5. **Continuous Deployment**: Automated documentation publication - -### Test Automation Suite -```bash -# Smoke test - default configuration -cookiecutter . --no-input - -# Integration test - custom configuration matrix -cookiecutter . --no-input \ - full_name="Test User" \ - project_name="Test Project" \ - project_short_description="Testing the template" - -# Quality assurance pipeline -cd test-project -task test # Unit test execution -task lint # Static code analysis -task static-check # Type safety validation -``` - -### Quality Assurance Gates -Release criteria checklist: -- **Template Variables**: All cookiecutter substitutions validated -- **Configuration Files**: TOML/YAML syntax and schema validation -- **Agent Infrastructure**: OpenCode components properly formatted with valid frontmatter -- **Generated Project Quality**: Full test suite passes (unit, integration, e2e) -- **Documentation Build**: Successful static site generation and deployment - -## Template Infrastructure Management - -### Separation of Concerns -- **Template Repository**: Infrastructure as Code for project generation -- **Generated Projects**: Independent project instances with embedded workflows -- **Skill Inheritance**: Development workflows packaged and distributed via template -- **Documentation Strategy**: Template docs for consumers; project docs for end-users - -### Version Control and Migration Strategy -- **Release Cadence**: Semantic versioning with automated changelog generation -- **Backward Compatibility**: Non-breaking changes in minor releases -- **Migration Paths**: Documentation for upgrading between major versions -- **Quality Standards**: Enforced through automated testing pipeline - -## DevOps Playbooks - -### Standard Release Pipeline -```bash -# 1. Execute comprehensive test suite -@template-manager /skill template-test - -# 2. Initiate release automation -@template-manager /skill template-release - -# 3. Documentation deployment (automated within release pipeline) -``` - -### Hotfix Deployment Process -```bash -# 1. Implement critical fix on hotfix branch -# 2. Execute targeted regression testing -@template-manager /skill template-test --scope=affected - -# 3. Fast-track patch release -@template-manager /skill template-release --type=patch --priority=critical - -# 4. Stakeholder notification via release notes -``` - -## Professional Standards - -As a DevOps Engineer, you maintain enterprise-grade standards for the cookiecutter template infrastructure: -- **Reliability**: Zero-downtime deployments and rollback capabilities -- **Security**: Supply chain security for distributed templates -- **Performance**: Optimized template generation and testing pipelines -- **Observability**: Comprehensive logging and metrics for template usage -- **Documentation**: Clear, versioned documentation for all stakeholders \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/code-quality/SKILL.md b/.opencode/skills/code-quality/SKILL.md similarity index 98% rename from {{cookiecutter.project_slug}}/.opencode/skills/code-quality/SKILL.md rename to .opencode/skills/code-quality/SKILL.md index e259036..35f7187 100644 --- a/{{cookiecutter.project_slug}}/.opencode/skills/code-quality/SKILL.md +++ b/.opencode/skills/code-quality/SKILL.md @@ -77,7 +77,7 @@ task test-report ``` #### Coverage Requirements -- Minimum coverage: {{cookiecutter.minimum_coverage}}% +- Minimum coverage: 100% - Excludes: pragma no cover, debug code, __repr__, assertions - Fails build if coverage drops below minimum @@ -190,7 +190,7 @@ uv run task mut-report # Generates: docs/mut_report.html # Targeted run on a specific module (faster feedback) -uv run cosmic-ray run cosmic-ray.toml {{cookiecutter.module_name}} tests/unit/ --report +uv run cosmic-ray run cosmic-ray.toml python_module_template tests/unit/ --report uv run cosmic-ray html-report cosmic-ray.toml > docs/mut_report.html ``` @@ -198,7 +198,7 @@ uv run cosmic-ray html-report cosmic-ray.toml > docs/mut_report.html ```toml [cosmic-ray] -module-path = "{{cookiecutter.module_name}}" +module-path = "python_module_template" timeout = 10.0 excluded-modules = [] # Add web/adapter modules to skip test-command = "uv run pytest tests/unit/ -x -q" @@ -324,7 +324,7 @@ jobs: - Examples in docstrings for complex functions #### Test Quality Metrics -- Line coverage: {{cookiecutter.minimum_coverage}}% +- Line coverage: 100% - Branch coverage: >90% - Mutation score: >80% (when mutation testing enabled) - Test-to-code ratio: >1:1 diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/create-agent/SKILL.md b/.opencode/skills/create-agent/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/create-agent/SKILL.md rename to .opencode/skills/create-agent/SKILL.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/create-skill/SKILL.md b/.opencode/skills/create-skill/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/create-skill/SKILL.md rename to .opencode/skills/create-skill/SKILL.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/epic-workflow/SKILL.md b/.opencode/skills/epic-workflow/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/epic-workflow/SKILL.md rename to .opencode/skills/epic-workflow/SKILL.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/feature-definition/SKILL.md b/.opencode/skills/feature-definition/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/feature-definition/SKILL.md rename to .opencode/skills/feature-definition/SKILL.md diff --git a/.opencode/skills/git-release/SKILL.md b/.opencode/skills/git-release/SKILL.md index 8798bab..d73bf57 100644 --- a/.opencode/skills/git-release/SKILL.md +++ b/.opencode/skills/git-release/SKILL.md @@ -75,6 +75,7 @@ The release name is generated by the AI based on analyzing the merged PRs and th ### Examples +Instead of pre-defined categories, the AI decides: ``` v1.2.20260315 - Blooming Narwhal (based on new features, growth theme) v1.3.20260316 - Velvet Manta (based on smooth refactoring) @@ -126,15 +127,21 @@ git checkout -b release/v1.3.${current_date} last_tag=$(git describe --tags --abbrev=0) gh pr list --state merged --base develop --limit 20 -# Example analysis output: +# Use YOUR AI to analyze the PR titles and descriptions: +# - Read each PR title and description +# - Understand what the changes are actually about +# - Determine the dominant theme/vibe +# - Select an appropriate adjective-animal pair + +# Example (you must do real analysis): # Recent PRs: -# - "Optimize database query performance" -# - "Add caching layer for API responses" -# - "Improve search algorithm efficiency" -# - "Speed up test suite execution" +# - "Add session-workflow skill for multi-session AI development" +# - "Remove BDD references and DEVELOPMENT_WORKFLOW.md" +# - "Template hotfix - Jinja2 escaping" # -# Dominant theme: PERFORMANCE (4 performance-related PRs) -# Selected name: "swift cheetah" +# AI Analysis: These are primarily about FEATURES and IMPROVEMENTS +# The addition of a new skill is the dominant theme +# Selected name: "creative fox" (features theme) ``` ### Step 3: Update Version and Changelog @@ -218,18 +225,21 @@ git commit -m "fix(security): patch authentication vulnerability" gh pr create --title "Critical Security Hotfix" \ --body "Emergency patch for authentication vulnerability" -# After merge, create revision release +# After merge, create minor release for same day current_date=$(date +%Y%m%d) last_version=$(git describe --tags --abbrev=0) -# Calculate next revision (v1.3.20260302 โ†’ v1.3.20260302r2) -next_revision=$(echo $last_version | sed 's/r\([0-9]\+\)/r\1+1/') +# Calculate next minor version (v1.3.20260302 โ†’ v1.4.20260302 if same day) +minor=$(echo $last_version | cut -d. -f2) +date_str=$(echo $last_version | cut -d. -f3) +new_minor=$((minor + 1)) +next_version="v${new_minor}.${date_str}" -git tag $next_revision +git tag $next_version git push origin main --tags -gh release create $next_revision \ - --title "$next_revision - Guardian Bear (Hotfix)" \ +gh release create $next_version \ + --title "$next_version - Guardian Bear (Hotfix)" \ --notes "Emergency security patch" ``` @@ -253,7 +263,7 @@ task static-check || { echo "โŒ Type checking failed"; exit 1; } # Validate version format version=$(grep 'version =' pyproject.toml | cut -d'"' -f2) -if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]{8}r[0-9]+$ ]]; then +if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]{8}$ ]]; then echo "โŒ Invalid version format: $version" exit 1 fi diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/implementation/SKILL.md b/.opencode/skills/implementation/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/implementation/SKILL.md rename to .opencode/skills/implementation/SKILL.md diff --git a/.opencode/skills/pr-management/SKILL.md b/.opencode/skills/pr-management/SKILL.md index f4a9ad2..e8060a5 100644 --- a/.opencode/skills/pr-management/SKILL.md +++ b/.opencode/skills/pr-management/SKILL.md @@ -491,7 +491,7 @@ fi ``` ### With CI/CD Pipeline -{% raw %} + ```yaml # Auto-deployment for specific labels name: Auto Deploy @@ -509,4 +509,3 @@ jobs: echo "Deploying PR #${{ github.event.pull_request.number }} to staging" # Deployment commands... ``` -{% endraw %} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/prototype-script/SKILL.md b/.opencode/skills/prototype-script/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/prototype-script/SKILL.md rename to .opencode/skills/prototype-script/SKILL.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/reference/prototype-patterns.md b/.opencode/skills/reference/prototype-patterns.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/reference/prototype-patterns.md rename to .opencode/skills/reference/prototype-patterns.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/reference/test-patterns.md b/.opencode/skills/reference/test-patterns.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/reference/test-patterns.md rename to .opencode/skills/reference/test-patterns.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/session-workflow/SKILL.md b/.opencode/skills/session-workflow/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/session-workflow/SKILL.md rename to .opencode/skills/session-workflow/SKILL.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/signature-design/SKILL.md b/.opencode/skills/signature-design/SKILL.md similarity index 100% rename from {{cookiecutter.project_slug}}/.opencode/skills/signature-design/SKILL.md rename to .opencode/skills/signature-design/SKILL.md diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/tdd/SKILL.md b/.opencode/skills/tdd/SKILL.md similarity index 92% rename from {{cookiecutter.project_slug}}/.opencode/skills/tdd/SKILL.md rename to .opencode/skills/tdd/SKILL.md index 1571c16..8f549cc 100644 --- a/{{cookiecutter.project_slug}}/.opencode/skills/tdd/SKILL.md +++ b/.opencode/skills/tdd/SKILL.md @@ -166,18 +166,18 @@ tests/ โ”‚ โ”œโ”€โ”€ __init__.py โ”‚ โ””โ”€โ”€ [entity]_repo_test.py โ”œโ”€โ”€ conftest.py # Shared fixtures -โ””โ”€โ”€ {{cookiecutter.project_slug}}_test.py # Smoke test +โ””โ”€โ”€ python-project-template_test.py # Smoke test ``` ### Mirror Source Tree Rule -For each source module `{{cookiecutter.module_name}}//.py`, create a corresponding test file `tests//_test.py`: +For each source module `python_module_template//.py`, create a corresponding test file `tests//_test.py`: | Source | Test | |--------|------| -| `{{cookiecutter.module_name}}/models.py` | `tests/unit/models_test.py` | -| `{{cookiecutter.module_name}}/domain/.py` | `tests/unit/domain/_test.py` | -| `{{cookiecutter.module_name}}/storage/factory.py` | `tests/integration/storage/factory_test.py` | -| `{{cookiecutter.module_name}}/storage/memory/adapters.py` | `tests/integration/storage/memory/*_repo_test.py` | +| `python_module_template/models.py` | `tests/unit/models_test.py` | +| `python_module_template/domain/.py` | `tests/unit/domain/_test.py` | +| `python_module_template/storage/factory.py` | `tests/integration/storage/factory_test.py` | +| `python_module_template/storage/memory/adapters.py` | `tests/integration/storage/memory/*_repo_test.py` | ## Test Workflow diff --git a/.opencode/skills/template-release/SKILL.md b/.opencode/skills/template-release/SKILL.md deleted file mode 100644 index 69b8943..0000000 --- a/.opencode/skills/template-release/SKILL.md +++ /dev/null @@ -1,350 +0,0 @@ ---- -name: template-release -description: Manage complete releases of the cookiecutter template with testing, documentation, themed naming and semantic versioning -license: MIT -compatibility: opencode -metadata: - audience: template-maintainers - workflow: template-management ---- -## What I do -Handle the complete release process for the cookiecutter template repository including version management, testing, documentation deployment, and GitHub releases. - -## When to use me -Use this when ready to release a new version of the cookiecutter template after making improvements, adding features, or fixing bugs. - -## Template Versioning Strategy - -### Hybrid Calver Versioning for Template -Use hybrid versioning: `v{major}.{minor}.{YYYYMMDD}` - -**Version Bump Guidelines:** -- **Major (v2.x.20260401)**: Breaking changes to cookiecutter variables, major workflow changes, removed features -- **Minor (v1.x.20260315)**: New agents, new skills, workflow enhancements, new features, or same-day releases - -**Examples:** -``` -v1.0.20260302 # Initial release on March 2, 2026 -v1.1.20260315 # Added repo-manager agent and git-release skill on March 15 -v1.2.20260315 # Second release same day (increment minor) -v1.3.20260320 # Added template-manager meta agent on March 20 -v2.0.20260401 # Changed cookiecutter.json structure (breaking) on April 1 -``` - -## Release Process Workflow - -### Phase 1: Pre-Release Validation -```bash -# 1. Run comprehensive template tests -echo "๐Ÿงช Running template tests..." -bash scripts/template_test.sh - -# 2. Validate template repository structure -echo "๐Ÿ” Validating template structure..." -test -f cookiecutter.json || { echo "โŒ Missing cookiecutter.json"; exit 1; } -test -d "{{cookiecutter.project_slug}}" || { echo "โŒ Missing template directory"; exit 1; } -test -f README.md || { echo "โŒ Missing template README"; exit 1; } - -# 3. Check for unresolved TODOs or FIXMEs -echo "๐Ÿ“ Checking for unresolved items..." -if grep -r "TODO\|FIXME" . --exclude-dir=.git --exclude="*.md" | grep -v "example"; then - echo "โš ๏ธ Found unresolved TODO/FIXME items - consider addressing before release" -fi -``` - -### Phase 2: Version Calculation and Update -```bash -# Get current version from git tags -current_version=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.00000000") - -# Calculate new version -current_date=$(date +%Y%m%d) -current_major=$(echo $current_version | sed 's/v\([0-9]\+\)\..*/\1/') -current_minor=$(echo $current_version | sed 's/v[0-9]\+\.\([0-9]\+\).*/\1/') -current_date_in_tag=$(echo $current_version | sed 's/v[0-9]\+\.[0-9]\+\.\([0-9]\{8\}\).*/\1/') - -case $bump_type in - "major") - new_version=$(echo "v$((current_major + 1)).0.${current_date}") - ;; - "minor") - # If same day as last release, increment minor further - if [ "$current_date_in_tag" = "$current_date" ]; then - new_version=$(echo "v${current_major}.$((current_minor + 2)).${current_date}") - else - new_version=$(echo "v${current_major}.$((current_minor + 1)).${current_date}") - fi - ;; - "revision") - if [ "$current_date_in_tag" = "$current_date" ]; then - new_version=$(echo "v${current_major}.$((current_minor + 1)).${current_date}") - else - new_version=$(echo "v${current_major}.${current_minor}.${current_date}") - fi - ;; -esac - -echo "๐Ÿ”– Next version: $new_version ($bump_type bump)" -``` - -### Phase 3: Update Documentation and Changelog -```bash -# Update CHANGELOG.md -echo "๐Ÿ“ Updating CHANGELOG.md..." -cat > CHANGELOG_ENTRY.tmp << EOF -## [$new_version] - $(date +%Y-%m-%d) - -### Added -$(git log ${current_version}..HEAD --grep="feat:" --pretty="- %s" | sed 's/feat: //') - -### Changed -$(git log ${current_version}..HEAD --grep="refactor:" --pretty="- %s" | sed 's/refactor: //') - -### Fixed -$(git log ${current_version}..HEAD --grep="fix:" --pretty="- %s" | sed 's/fix: //') - -### Removed -$(git log ${current_version}..HEAD --grep="BREAKING CHANGE" --pretty="- %s") - -EOF - -# Insert at top of CHANGELOG.md (after header) -if [ -f CHANGELOG.md ]; then - sed -i '2r CHANGELOG_ENTRY.tmp' CHANGELOG.md -else - echo "# Changelog\n\nAll notable changes to this cookiecutter template will be documented in this file.\n" > CHANGELOG.md - cat CHANGELOG_ENTRY.tmp >> CHANGELOG.md -fi - -rm CHANGELOG_ENTRY.tmp - -# Update README.md with latest version info if needed -sed -i "s/Version: v[0-9]\+\.[0-9]\+\.[0-9]\+/Version: $new_version/g" README.md -``` - -### Phase 4: Final Template Test with New Version -```bash -# Test template generation one final time -echo "๐Ÿงช Final template validation..." -cookiecutter . --no-input - -# Validate generated project -generated_project=$(ls -d */ | grep -v ".git" | head -1 | sed 's/\///') -cd "$generated_project" - -# Quick smoke test -python -m venv venv -source venv/bin/activate -pip install uv -uv pip install '.[dev]' -python -m ruff check . || { echo "โŒ Generated project linting failed"; exit 1; } - -cd .. -rm -rf "$generated_project" -echo "โœ… Final validation passed" -``` - -### Phase 5: Create Release with Themed Naming - -**IMPORTANT**: You must use your AI capabilities to analyze the commits and generate an appropriate themed name. Do NOT use random/hardcoded selection. - -1. **Get commits since last release:** -```bash -git log ${current_version}..HEAD --oneline -``` - -2. **Analyze the commits using your AI** to determine what this release is about: - - Read each commit message and PR description - - Understand the story/narrative of the release - -3. **Generate a unique themed name** based on your analysis: - - Create an adjective-animal pair that reflects the content - - Avoid overused combinations (fox, owl, dolphin, cheetah) - - Make it creative and memorable - -**Good examples**: Blooming Narwhal, Crystal Jellyfish, Velvet Manta, Electric Firefly, Aurora Moth, Tidal Otter - -4. **Create the release:** -```bash -# Commit version changes -git add CHANGELOG.md README.md -git commit -m "chore(release): prepare $new_version - -- Update changelog with latest changes -- Bump version references in documentation -- Final template validation completed" - -# Create annotated tag -git tag -a $new_version -m "Release $new_version \"$release_name\" - -$(git log ${current_version}..HEAD --oneline | head -10) - -See CHANGELOG.md for complete details." - -# Push changes and tags -git push origin main -git push origin $new_version - -# Create GitHub release with themed name (use the AI-generated name as title) -changelog_section=$(sed -n "/## \[$new_version\]/,/## \[/p" CHANGELOG.md | head -n -1) - -gh release create $new_version \ - --title "$release_name" \ - --notes "$changelog_section" \ - --latest -``` - -### Phase 6: Documentation Deployment -```bash -# Deploy documentation using the template's own task -# Note: This uses the generated project's doc-publish task as reference - -echo "๐Ÿ“– Deploying documentation..." - -# If template has its own docs directory -if [ -d "docs" ]; then - # Build and deploy documentation - pip install mkdocs mkdocs-material - mkdocs gh-deploy --force - echo "โœ… Documentation deployed to GitHub Pages" -else - echo "โ„น๏ธ No template documentation directory found" -fi - -# Update template README with latest release info -echo "๐Ÿ“ Updating template README with release info..." -sed -i "s/Latest Release: .*/Latest Release: [$new_version](https:\/\/github.com\/$(gh repo view --json owner,name --jq '.owner.login + \"/\" + .name')\/releases\/tag\/$new_version)/" README.md - -git add README.md -git commit -m "docs: update README with latest release info" -git push origin main -``` - -## Release Types and Procedures - -### Regular Release (Minor/Patch) -```bash -# Standard release workflow -@template-manager /skill template-test # Validate template -@template-manager /skill template-release --type=minor -``` - -### Emergency Patch Release -```bash -# Hot fix workflow -git checkout main -git checkout -b hotfix/critical-template-bug -# ... make minimal fixes ... -git commit -m "fix(template): resolve critical cookiecutter generation issue" -git push origin hotfix/critical-template-bug - -gh pr create --title "Critical Template Fix" --body "Emergency fix for template generation" -# After merge: -@template-manager /skill template-release --type=patch --emergency -``` - -### Major Release (Breaking Changes) -```bash -# Breaking changes require careful handling -@template-manager /skill template-test # Extra validation -# Review breaking change documentation -@template-manager /skill template-release --type=major -# Send notification to template users -``` - -## Release Validation Checklist - -### Pre-Release Requirements -- [ ] All template tests pass (`/skill template-test`) -- [ ] Generated projects pass quality checks -- [ ] CHANGELOG.md is updated -- [ ] README.md reflects current state -- [ ] No unresolved TODO/FIXME items -- [ ] Documentation builds successfully -- [ ] Version bump type is appropriate - -### Post-Release Validation -- [ ] GitHub release created successfully -- [ ] Documentation deployed to GitHub Pages -- [ ] Template can be used immediately: `cookiecutter gh:username/repo` -- [ ] Generated projects work correctly -- [ ] Release announcement prepared (if needed) - -### Breaking Change Considerations -For major releases with breaking changes: -- [ ] Migration guide created -- [ ] Breaking changes documented in CHANGELOG -- [ ] Deprecation warnings added (if possible) -- [ ] Communication plan for existing users - -## Integration with Generated Project Releases - -### Template vs Project Release Coordination -```bash -# Template releases create new versions of the workflow -# Generated projects inherit the workflow but version independently - -# Template release workflow: -# 1. Template v1.2.0 introduces new agent -# 2. New projects get the new agent automatically -# 3. Existing projects can regenerate to get updates - -# Communication between systems: -echo "Template Release: $new_version" >> .template-releases -echo "Compatible with generated project workflow: v1.x.x" >> .template-releases -``` - -### Automated Release Notifications -```bash -# Create release announcement -cat > RELEASE_ANNOUNCEMENT.md << EOF -# ๐Ÿš€ Python Project Template $new_version Released - -## What's New -$changelog_section - -## How to Use -\`\`\`bash -cookiecutter gh:your-username/python-project-template -\`\`\` - -## Upgrading Existing Projects -To get the latest features in existing projects: -1. Backup your current project -2. Regenerate from the new template -3. Merge changes carefully - -See [Migration Guide](docs/migration.md) for details. -EOF -``` - -## Example Release Commands - -### Standard Minor Release -```bash -# After adding new skills/agents -git add . -git commit -m "feat(agents): add template-manager meta agent" -@template-manager /skill template-release -# Output: "Created release v1.2.20260320 with new meta agent functionality" -``` - -### Patch Release -```bash -# After fixing documentation or bugs -git add . -git commit -m "fix(docs): correct cookiecutter variable examples" -@template-manager /skill template-release -# Output: "Created release v1.2.20260320 with documentation fixes" -``` - -### Major Release -```bash -# After changing cookiecutter.json structure -git add . -git commit -m "feat!: restructure cookiecutter variables for better usability - -BREAKING CHANGE: cookiecutter.json format changed" -@template-manager /skill template-release -# Output: "Created release v2.0.20260401 with breaking changes - migration guide included" -``` \ No newline at end of file diff --git a/.opencode/skills/template-test/SKILL.md b/.opencode/skills/template-test/SKILL.md deleted file mode 100644 index 5740012..0000000 --- a/.opencode/skills/template-test/SKILL.md +++ /dev/null @@ -1,403 +0,0 @@ ---- -name: template-test -description: Test cookiecutter template generation with automatic responses and validate generated project quality -license: MIT -compatibility: opencode -metadata: - audience: template-maintainers - workflow: template-management ---- -## What I do -Test the cookiecutter template generation process with various configurations, validate the generated projects work correctly, and ensure all OpenCode agents/skills function properly. - -## When to use me -Use this before any template release, after making changes to template files, or when validating template functionality. - -## Template Testing Strategy - -### Test Scenarios - -#### 1. Default Generation Test -```bash -# Test with all default values from cookiecutter.json -cookiecutter . --no-input - -# Expected output directory: python-project-example/ -# (based on default project_name in cookiecutter.json) -``` - -#### 2. Custom Values Test -```bash -# Test with custom configuration -cookiecutter . --no-input \ - full_name="Jane Developer" \ - email="jane@example.com" \ - github_username="jane-dev" \ - project_name="My Awesome Project" \ - project_short_description="An amazing Python project using AI workflows" \ - minimum_coverage="90" \ - license="Apache_2.0" \ - version="0.2.0" - -# Expected output directory: my-awesome-project/ -``` - -#### 3. Edge Cases Test -```bash -# Test with edge case values -cookiecutter . --no-input \ - project_name="Project With Spaces And-Hyphens_Underscores" \ - project_short_description="A very long description that tests how the template handles lengthy text input and ensures it doesn't break any generated files or configurations" \ - minimum_coverage="100" \ - github_username="user-with-hyphens" - -# Expected output directory: project-with-spaces-and-hyphens-underscores/ -``` - -## Validation Checklist - -### Generated Project Structure -```bash -# Verify directory structure -test -d "${project_dir}" -test -d "${project_dir}/.opencode" -test -d "${project_dir}/.opencode/agents" -test -d "${project_dir}/.opencode/skills" -test -f "${project_dir}/pyproject.toml" -test -f "${project_dir}/README.md" -test -f "${project_dir}/AGENTS.md" -``` - -### File Content Validation -```bash -# Check cookiecutter variables were substituted correctly -grep -q "{{cookiecutter" "${project_dir}"/* && echo "ERROR: Unsubstituted variables found" - -# Validate pyproject.toml syntax -cd "${project_dir}" -python -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))" || echo "ERROR: Invalid pyproject.toml" - -# Check README has project name -grep -q "${project_name}" README.md || echo "ERROR: Project name not in README" -``` - -### OpenCode Agents Validation -```bash -# Test agent YAML frontmatter is valid -for agent in .opencode/agents/*.md; do - python -c " -import yaml -with open('$agent', 'r') as f: - content = f.read() - if '---' in content: - yaml_part = content.split('---')[1] - yaml.safe_load(yaml_part) -" || echo "ERROR: Invalid YAML in $agent" -done -``` - -### Skills Validation -```bash -# Test skill YAML frontmatter is valid -for skill in .opencode/skills/*/SKILL.md; do - python -c " -import yaml -with open('$skill', 'r') as f: - content = f.read() - if '---' in content: - yaml_part = content.split('---')[1] - yaml.safe_load(yaml_part) -" || echo "ERROR: Invalid YAML in $skill" -done - -# Check skill naming consistency -for skill_dir in .opencode/skills/*/; do - skill_name=$(basename "$skill_dir") - grep -q "name: $skill_name" "$skill_dir/SKILL.md" || echo "ERROR: Skill name mismatch in $skill_dir" -done -``` - -### Generated Project Quality Tests -```bash -# Install dependencies and run tests -cd "${project_dir}" -python -m venv venv -source venv/bin/activate -pip install uv -uv pip install '.[dev]' - -# Run all quality checks that should pass in generated project -task test || echo "ERROR: Tests failed in generated project" -task lint || echo "ERROR: Linting failed in generated project" -task static-check || echo "ERROR: Type checking failed in generated project" - -# Test documentation generation -task doc-report || echo "ERROR: Documentation build failed" - -# Verify coverage meets minimum requirement -coverage_result=$(coverage report --format=total) -min_coverage=$(grep 'minimum_coverage' pyproject.toml | grep -o '[0-9]\+') -if [ "$coverage_result" -lt "$min_coverage" ]; then - echo "ERROR: Coverage $coverage_result% below minimum $min_coverage%" -fi -``` - -## Complete Testing Script - -### Automated Test Runner -```bash -#!/bin/bash -# template_test.sh - Comprehensive template testing script - -set -e - -# Configuration -TEMPLATE_DIR=$(pwd) -TEST_DIR="/tmp/template-tests" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) - -# Clean up previous tests -rm -rf "$TEST_DIR" -mkdir -p "$TEST_DIR" -cd "$TEST_DIR" - -echo "๐Ÿงช Starting cookiecutter template tests at $TIMESTAMP" - -# Test 1: Default configuration -echo "๐Ÿ“‹ Test 1: Default configuration" -cookiecutter "$TEMPLATE_DIR" --no-input -default_project=$(ls -d */ | head -1 | sed 's/\///') -echo "โœ… Generated project: $default_project" - -# Test 2: Custom configuration -echo "๐Ÿ“‹ Test 2: Custom configuration" -cookiecutter "$TEMPLATE_DIR" --no-input \ - full_name="Test Developer" \ - email="test@example.com" \ - github_username="test-dev" \ - project_name="Custom Test Project" \ - project_short_description="Testing template with custom values" \ - minimum_coverage="95" \ - license="MIT" \ - version="1.0.0" - -custom_project="custom-test-project" -echo "โœ… Generated project: $custom_project" - -# Test 3: Edge cases -echo "๐Ÿ“‹ Test 3: Edge case configuration" -cookiecutter "$TEMPLATE_DIR" --no-input \ - project_name="Edge Case Project With Long Name" \ - project_short_description="This is a very long description to test edge cases and ensure the template handles it properly without breaking anything" \ - minimum_coverage="100" \ - github_username="edge-case-user" - -edge_project="edge-case-project-with-long-name" -echo "โœ… Generated project: $edge_project" - -# Validation function -validate_project() { - local project_dir=$1 - local project_name=$2 - - echo "๐Ÿ” Validating $project_dir..." - - cd "$project_dir" - - # Structure validation - test -d ".opencode" || { echo "โŒ Missing .opencode directory"; return 1; } - test -d ".opencode/agents" || { echo "โŒ Missing .opencode/agents"; return 1; } - test -d ".opencode/skills" || { echo "โŒ Missing .opencode/skills"; return 1; } - test -f "pyproject.toml" || { echo "โŒ Missing pyproject.toml"; return 1; } - test -f "README.md" || { echo "โŒ Missing README.md"; return 1; } - test -f "AGENTS.md" || { echo "โŒ Missing AGENTS.md"; return 1; } - - # Check for unsubstituted variables - if grep -r "{{cookiecutter" . --exclude-dir=venv 2>/dev/null; then - echo "โŒ Found unsubstituted cookiecutter variables" - return 1 - fi - - # Validate pyproject.toml syntax - python -c " -import tomllib -try: - with open('pyproject.toml', 'rb') as f: - tomllib.load(f) - print('โœ… pyproject.toml is valid') -except Exception as e: - print(f'โŒ pyproject.toml is invalid: {e}') - exit(1) -" - - # Validate YAML frontmatter in agents - for agent in .opencode/agents/*.md; do - if [ -f "$agent" ]; then - python -c " -import yaml, sys -with open('$agent', 'r') as f: - content = f.read() - if '---' in content: - parts = content.split('---') - if len(parts) >= 3: - try: - yaml.safe_load(parts[1]) - print('โœ… Valid YAML in $agent') - except Exception as e: - print(f'โŒ Invalid YAML in $agent: {e}') - sys.exit(1) -" - fi - done - - # Validate YAML frontmatter in skills - for skill in .opencode/skills/*/SKILL.md; do - if [ -f "$skill" ]; then - python -c " -import yaml, sys -with open('$skill', 'r') as f: - content = f.read() - if '---' in content: - parts = content.split('---') - if len(parts) >= 3: - try: - yaml.safe_load(parts[1]) - print('โœ… Valid YAML in $skill') - except Exception as e: - print(f'โŒ Invalid YAML in $skill: {e}') - sys.exit(1) -" - fi - done - - # Check project name appears in key files - grep -q "$project_name" README.md || { echo "โŒ Project name not in README.md"; return 1; } - grep -q "$project_name" AGENTS.md || { echo "โŒ Project name not in AGENTS.md"; return 1; } - - # Install dependencies and test - echo "๐Ÿ“ฆ Installing dependencies..." - python -m venv venv - source venv/bin/activate - pip install uv - uv pip install '.[dev]' || { echo "โŒ Dependency installation failed"; return 1; } - - # Run quality checks - echo "๐Ÿ”ง Running quality checks..." - - # Linting - python -m ruff check . || { echo "โŒ Ruff linting failed"; return 1; } - echo "โœ… Linting passed" - - # Type checking - python -m pyright || { echo "โŒ Type checking failed"; return 1; } - echo "โœ… Type checking passed" - - # Tests - python -m pytest tests/ || { echo "โŒ Tests failed"; return 1; } - echo "โœ… Tests passed" - - # Documentation build - python -m mkdocs build || { echo "โŒ Documentation build failed"; return 1; } - echo "โœ… Documentation build passed" - - echo "โœ… Project $project_dir validation completed successfully" - cd .. - return 0 -} - -# Run validations -validate_project "$default_project" "Python Project Example" -validate_project "$custom_project" "Custom Test Project" -validate_project "$edge_project" "Edge Case Project With Long Name" - -echo "๐ŸŽ‰ All template tests passed successfully!" -echo "๐Ÿ“Š Test summary:" -echo " - Default configuration: โœ…" -echo " - Custom configuration: โœ…" -echo " - Edge case configuration: โœ…" -echo " - Structure validation: โœ…" -echo " - YAML validation: โœ…" -echo " - Quality checks: โœ…" -echo " - Generated projects work: โœ…" - -# Cleanup -echo "๐Ÿงน Cleaning up test artifacts..." -rm -rf "$TEST_DIR" -echo "โœ… Template testing completed successfully at $(date)" -``` - -## Integration with CI/CD - -### GitHub Actions Template Test -```yaml -# .github/workflows/template-test.yml -name: Template Testing - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - test-template: - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ['3.13'] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - - name: Install cookiecutter - run: | - pip install cookiecutter - - - name: Test default template generation - run: | - cookiecutter . --no-input - - - name: Test custom template generation - run: | - cookiecutter . --no-input \ - project_name="CI Test Project" \ - project_short_description="Testing in CI" \ - minimum_coverage="95" - - - name: Validate generated projects - run: | - # Run validation on both generated projects - for project in python-project-example ci-test-project; do - cd $project - python -m venv venv - source venv/bin/activate - pip install uv - uv pip install '.[dev]' - python -m ruff check . - python -m pyright - python -m pytest - cd .. - done -``` - -## Quality Metrics - -### Template Health Indicators -- โœ… All test scenarios pass -- โœ… Generated projects have 100% test coverage by default -- โœ… All YAML frontmatter is valid -- โœ… No unsubstituted cookiecutter variables -- โœ… Generated projects pass linting and type checking -- โœ… Documentation builds successfully -- โœ… All OpenCode agents and skills are properly formatted - -### Performance Benchmarks -- Template generation time: < 5 seconds -- Generated project setup time: < 30 seconds -- Quality check runtime: < 60 seconds -- Documentation build time: < 15 seconds \ No newline at end of file diff --git a/.opencode/templates/.github/dependabot.yml.template b/.opencode/templates/.github/dependabot.yml.template new file mode 100644 index 0000000..ef29d37 --- /dev/null +++ b/.opencode/templates/.github/dependabot.yml.template @@ -0,0 +1,50 @@ +version: 2 +updates: + # Python dependencies + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + # Group updates to reduce PR noise + groups: + pytest: + patterns: + - "pytest*" + dev-tools: + patterns: + - "ruff" + - "pyright" + - "mutmut" + - "pdoc" + security: + patterns: + - "detect-secrets" + - "safety" + main-deps: + patterns: + - "fire" + - "dotenv" + # Security updates get higher priority + open-pull-requests-limit: 10 + reviewers: + - "ORIGINAL_GITHUB_USERNAME" + labels: + - "dependencies" + - "automated" + + # GitHub Actions updates + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 5 + reviewers: + - "ORIGINAL_GITHUB_USERNAME" + labels: + - "github-actions" + - "dependencies" + - "automated" \ No newline at end of file diff --git a/.opencode/templates/.github/workflows/ci.yml.template b/.opencode/templates/.github/workflows/ci.yml.template new file mode 100644 index 0000000..84a758b --- /dev/null +++ b/.opencode/templates/.github/workflows/ci.yml.template @@ -0,0 +1,172 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + # Improve pip performance + PIP_NO_CACHE_DIR: false + PIP_USER: 1 + # uv settings + UV_SYSTEM_PYTHON: 1 + UV_CACHE_DIR: /tmp/.uv-cache + +jobs: + quality: + name: Code Quality & Security + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Run ruff linting (includes security checks) + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" ruff-check + + - name: Check ruff formatting + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" ruff-format --check + + - name: Run type checking + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" static-check + + - name: Check for secrets + run: | + if [ ! -f .secrets.baseline ]; then + echo "Creating secrets baseline..." + uv run detect-secrets scan --baseline .secrets.baseline + fi + # Check for new secrets since baseline + uv run detect-secrets scan --baseline .secrets.baseline + echo "โœ… Secret scanning complete - no new secrets detected" + + test: + name: Tests + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.12", "3.13"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Run fast tests + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" test-fast + + - name: Run full test suite (main branch and PRs only) + if: github.ref == 'refs/heads/main' || github.event_name == 'pull_request' + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" test + + - name: Upload coverage reports + if: matrix.python-version == '3.13' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + docs/coverage/ + docs/tests/ + retention-days: 30 + + - name: Upload coverage to Codecov (optional) + if: matrix.python-version == '3.13' && (github.ref == 'refs/heads/main' || github.event_name == 'pull_request') + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml + fail_ci_if_error: false + verbose: true + + build: + name: Build & Documentation + runs-on: ubuntu-latest + needs: [quality, test] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python 3.13 + run: uv python install 3.13 + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Build documentation + run: uv run python -c "import taskipy.cli; taskipy.cli.main()" doc-build + + - name: Build package + run: uv build + + - name: Verify package installation (wheel) + run: | + uv run --isolated --no-project --with dist/*.whl python -c "import python_package_template; print('โœ“ Wheel install successful')" + + - name: Verify package installation (sdist) + run: | + uv run --isolated --no-project --with dist/*.tar.gz python -c "import python_package_template; print('โœ“ Source dist install successful')" + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + retention-days: 30 + + - name: Upload documentation + uses: actions/upload-artifact@v4 + with: + name: documentation + path: docs/api/ + retention-days: 30 + + # Security summary job + security-summary: + name: Security Summary + runs-on: ubuntu-latest + needs: [quality] + if: always() + + steps: + - name: Security Summary + run: | + echo "## ๐Ÿ”’ Security Scan Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### โœ… Ruff Security Analysis" >> $GITHUB_STEP_SUMMARY + echo "Security rules (S001-S701) checked during linting phase." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Additional Security Features" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Secret scanning (detect-secrets)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Dependency vulnerability scanning (safety + dependabot)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… CodeQL security analysis (weekly + on pushes)" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Comprehensive security rules via Ruff (flake8-bandit S001-S701)" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.opencode/templates/.github/workflows/codeql.yml.template b/.opencode/templates/.github/workflows/codeql.yml.template new file mode 100644 index 0000000..3d548f1 --- /dev/null +++ b/.opencode/templates/.github/workflows/codeql.yml.template @@ -0,0 +1,59 @@ +name: "CodeQL Security Analysis" + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + # Run CodeQL analysis weekly on Sundays at 6:00 AM UTC + - cron: '0 6 * * 0' + +jobs: + analyze: + name: Analyze Code + runs-on: ubuntu-latest + timeout-minutes: 360 + permissions: + # Required for all workflows + security-events: write + # Required for workflows in public repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: python + build-mode: none # CodeQL supports 'none' for Python (interpreted language) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # Specify additional queries to run + queries: +security-and-quality + + # For Python, no build step is required as CodeQL analyzes source code directly + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" + + - name: Upload CodeQL results summary + run: | + echo "## ๐Ÿ” CodeQL Security Analysis Complete" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Analysis Details" >> $GITHUB_STEP_SUMMARY + echo "- **Language**: ${{ matrix.language }}" >> $GITHUB_STEP_SUMMARY + echo "- **Build Mode**: ${{ matrix.build-mode }}" >> $GITHUB_STEP_SUMMARY + echo "- **Queries**: security-and-quality" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Results are available in the Security tab of this repository." >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.opencode/templates/.github/workflows/dependency-review.yml.template b/.opencode/templates/.github/workflows/dependency-review.yml.template new file mode 100644 index 0000000..62d3351 --- /dev/null +++ b/.opencode/templates/.github/workflows/dependency-review.yml.template @@ -0,0 +1,24 @@ +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v4 + + - name: 'Dependency Review' + uses: actions/dependency-review-action@v4 + with: + # Fail if any vulnerabilities are found + fail-on-severity: moderate + # Allow certain licenses (common permissive licenses) + allow-licenses: GPL-2.0, GPL-3.0, LGPL-2.1, LGPL-3.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC, Apache-2.0, Unlicense, 0BSD + # Deny problematic licenses + deny-licenses: AGPL-1.0, AGPL-3.0, GPL-1.0 + # Comment on PR with results + comment-summary-in-pr: always \ No newline at end of file diff --git a/.opencode/templates/.secrets.baseline.template b/.opencode/templates/.secrets.baseline.template new file mode 100644 index 0000000..3ecd45c --- /dev/null +++ b/.opencode/templates/.secrets.baseline.template @@ -0,0 +1,115 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": {}, + "generated_at": "2026-04-11T17:00:00Z" +} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/AGENTS.md b/.opencode/templates/AGENTS.md.template similarity index 84% rename from {{cookiecutter.project_slug}}/AGENTS.md rename to .opencode/templates/AGENTS.md.template index 68af388..515c09e 100644 --- a/{{cookiecutter.project_slug}}/AGENTS.md +++ b/.opencode/templates/AGENTS.md.template @@ -1,21 +1,6 @@ -# {{cookiecutter.project_name}} +# Python Project Template -{{cookiecutter.project_short_description}} - -## Project Details - -| Variable | Value | -|----------|-------| -| **Project Name** | {{cookiecutter.project_name}} | -| **Project Slug** | {{cookiecutter.project_slug}} | -| **Package Name** | {{cookiecutter.package_name}} | -| **Module Name** | {{cookiecutter.module_name}} | -| **Author** | {{cookiecutter.full_name}} | -| **Email** | {{cookiecutter.email}} | -| **GitHub** | @{{cookiecutter.github_username}} | -| **License** | {{cookiecutter.license}} | -| **Version** | {{cookiecutter.version}} | -| **Minimum Coverage** | {{cookiecutter.minimum_coverage}}% | +Python template with some awesome tools to quickstart any Python project ## Session-Based Development @@ -60,17 +45,10 @@ This project includes custom skills for OpenCode: ## Available Agents -<<<<<<< HEAD -- **developer**: Main development agent with complete TDD workflow and QA integration +- **developer**: Main development agent with complete 7-phase TDD workflow and QA integration - **architect**: Software architect for design review, pattern selection, and SOLID compliance - **requirements-gatherer**: Business analyst for requirements elicitation and feature analysis - **overseer**: Quality assurance specialist enforcing standards at mandatory checkpoints -======= -- **developer**: Main development agent with complete 7-phase TDD workflow -- **architect**: Design review and approval agent for SOLID/object calisthenics compliance -- **overseer**: Quality assurance agent - reviews work after each test implementation, requests changes if needed -- **requirements-gatherer**: Gathers project requirements, updates documentation, creates analysis for architect ->>>>>>> origin/main - **repo-manager**: Repository management for Git operations, PRs, commits, and releases ## Development Commands @@ -105,6 +83,20 @@ task doc-serve task doc-build ``` +## Docker Commands + +```bash +# Development with Docker +docker-compose up # Start development environment +docker-compose --profile test up # Run test suite +docker-compose --profile quality up # Code quality checks + +# Production +docker build --target production -t app:prod . # Build production image +docker-compose -f docker-compose.prod.yml up # Production testing +docker-compose -f docker-compose.prod.yml --profile security up # Security scan +``` + ## Documentation This project uses **pdoc** for API documentation generation: @@ -123,10 +115,13 @@ Generated docs are in `docs/api/` - open `docs/api/index.html` to browse. This project uses BDD-style tests with the following conventions: +### Test File Naming + +Test filenames should follow _test.py + ### Test Function Naming ```python # Format: test__should_ -def test_given__when__then_(): ... def test__should_(): ... ``` @@ -155,14 +150,14 @@ task test ``` ### Checking Test Compliance -- **pytest-html-plus report**: `docs/tests/report.html` - BDD docstrings displayed as test names +- **pytest-html report**: `docs/tests/report.html` - BDD docstrings displayed - **Coverage report**: `docs/coverage/index.html` - View coverage by file ## Code Quality Standards - **Linting**: ruff with Google style conventions (D205, D212, D415 disabled for test files to allow BDD docstrings) - **Type Checking**: pyright -- **Test Coverage**: Minimum {{cookiecutter.minimum_coverage}}% +- **Test Coverage**: Minimum 100% - **Python Version**: >=3.13 - **Test Markers**: `slow` marks tests >50ms (SQLite, Hypothesis, web routes) @@ -181,7 +176,7 @@ Releases use AI-generated adjective-animal names. Each release gets a unique nam - `v1.6.20260404 - Velvet Manta` (refactoring) - `v1.7.20260405 - Electric Firefly` (performance) -The AI analyzes PR content and generates creative, unique names. +The AI analyzes PR content and generates creative, unique names containing a characteristic/adjectiv and an animal name ### Creating Releases Use the repo-manager agent: @@ -210,10 +205,7 @@ Then run `/init` to generate a fresh `AGENTS.md` based on your project's current ``` #### Epic-based feature development with QA gates -```bash -<<<<<<< HEAD -# For each feature in the epic: -======= + # 0. Gather requirements first (for new projects) @requirements-gatherer # Ask questions, create analysis, update docs @architect # Review analysis and approve design @@ -229,7 +221,6 @@ Then run `/init` to generate a fresh `AGENTS.md` based on your project's current @developer /skill implementation @developer /skill code-quality @overseer # Final review before moving on ->>>>>>> origin/main # 1. Requirements & Analysis @requirements-gatherer # Gather detailed requirements diff --git a/.opencode/templates/CHANGELOG.md.template b/.opencode/templates/CHANGELOG.md.template new file mode 100644 index 0000000..f1775dd --- /dev/null +++ b/.opencode/templates/CHANGELOG.md.template @@ -0,0 +1,61 @@ +# Changelog + +All notable changes to this template will be documented in this file. + +## [v1.7.20260410] - Vivid Cardinal - 2026-04-10 + +### Added +- **QA-gated Epic Workflow** - Complete epic-based development with mandatory quality checkpoints at each phase +- **Epic-workflow Skill** - Manages epic-based development with automatic feature progression +- **EPICS.md Template** - Epic tracking and management file for generated projects + +### Changed +- Updated all agent descriptions to use industry-standard roles (Development Lead, Software Architect, QA Specialist, Business Analyst, Release Engineer) +- Removed model specifications from all agents to make template model-agnostic +- Updated AGENTS.md to properly document all 5 generated project agents and all skills +- Updated README.md with new workflow and agent roles + +### Fixed +- Documentation now accurately reflects what exists in template + +## [v1.6.20260409] - Guardian Owl - 2026-04-09 + +### Added +- **Overseer Agent** - Quality assurance agent that reviews work after each test implementation and requests changes if needed +- **Requirements Gatherer Agent** - Agent that asks questions to understand project needs, updates documentation, creates detailed analysis for architect + +### Changed +- Updated developer workflow to include `@overseer` calls after Phase 3 (TDD tests) and Phase 7 (Quality Assurance) +- Updated AGENTS.md with new agents and updated workflow examples + +## [v1.0.0] - 2026-03-12 + +### Added +- **AI-Enhanced Development Workflow** - Complete OpenCode integration for AI-powered development +- **Developer Agent** - Main development agent with 7-phase TDD workflow +- **Architect Agent** - Design review agent for SOLID principles and object calisthenics compliance +- **Repository Manager Agent** - Git operations, PRs, and themed releases management +- **Development Skills** - feature-definition, prototype-script, tdd, signature-design, implementation, code-quality +- **Repository Skills** - git-release (hybrid calver versioning with themed releases), pr-management +- **Meta Skills** - create-skill, create-agent for extending OpenCode +- **Template Management** - template-manager agent, template-test, template-release skills +- **Comprehensive CI Workflow** - Template validation, generated project tests, Docker builds +- **Validation Scripts** - cookiecutter.json, pyproject.toml, YAML frontmatter validation + +### Changed +- Updated README.md with modern AI-focused branding +- Updated generated project README template with AI development workflow + +### Features +- **7-Phase Development Cycle**: Feature Definition โ†’ Prototype โ†’ TDD โ†’ Signature Design โ†’ Architecture Review โ†’ Implementation โ†’ Quality Assurance +- **SOLID Principles Enforcement** - Single responsibility, dependency inversion, interface segregation +- **Object Calisthenics** - No primitives, small classes, behavior-rich objects +- **Hybrid Calver Versioning**: v1.2.20260302 format with themed releases +- **Themed Release Names**: "Swift Cheetah", "Vigilant Owl", "Creative Fox" based on PR sentiment +- **Property-Based Testing**: Hypothesis integration for robust test coverage + +### Migration Notes +- This is the first semantic version release +- No breaking changes to cookiecutter.json structure +- Generated projects now include OpenCode agents and skills +- Existing projects can regenerate to get new features diff --git a/.opencode/templates/EPICS.md.template b/.opencode/templates/EPICS.md.template new file mode 100644 index 0000000..32552a8 --- /dev/null +++ b/.opencode/templates/EPICS.md.template @@ -0,0 +1,62 @@ +# Python Project Template - Epic Tracking + +This file tracks all epics and their features. Each feature goes through mandatory QA gates before proceeding to the next. + +**Status Legend**: โธ๏ธ Pending | ๐Ÿ”„ In Progress | โœ… Complete | โŒ Blocked + +--- + +## Epic: Project Foundation +**Status**: ๐Ÿ”„ In Progress +**Business Value**: Establish the core infrastructure and development workflows for the project + +### Features: +1. **Project Setup** - Status: โธ๏ธ Pending + - Acceptance Criteria: + - Development environment configured + - All dependencies installed and verified + - Base tests passing + - QA Status: Not Started + +2. **Development Workflow** - Status: โธ๏ธ Pending + - Acceptance Criteria: + - All agents and skills operational + - Epic/feature workflow established + - QA gates functioning + - QA Status: Not Started + +--- + +## Epic: [Your First Epic Name] +**Status**: โธ๏ธ Pending +**Business Value**: [Why this epic provides value to users/business] + +### Features: +1. **[Feature 1 Name]** - Status: โธ๏ธ Pending + - Acceptance Criteria: + - [Specific measurable criterion] + - [Another criterion] + - QA Status: Not Started + +2. **[Feature 2 Name]** - Status: โธ๏ธ Pending + - Acceptance Criteria: + - [Specific measurable criterion] + - [Another criterion] + - QA Status: Not Started + +--- + +## QA History + +| Date | Feature | Epic | QA Result | Reviewer | +|------|---------|------|-----------|----------| +| YYYY-MM-DD | Feature Name | Epic Name | Approved/Rejected | @overseer | + +--- + +## Notes + +- Each feature must pass all QA gates before marked complete +- Features automatically flow to the next upon completion +- Epics complete when all contained features are done +- Use `@developer /skill epic-workflow` to manage epic progression \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/README.md b/.opencode/templates/README.md.template similarity index 51% rename from {{cookiecutter.project_slug}}/README.md rename to .opencode/templates/README.md.template index 1b7bf77..b436b96 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/.opencode/templates/README.md.template @@ -1,13 +1,19 @@ -# {{cookiecutter.project_name}} +# {{PROJECT_NAME}} [![Contributors][contributors-shield]][contributors-url] [![Forks][forks-shield]][forks-url] [![Stargazers][stars-shield]][stars-url] [![Issues][issues-shield]][issues-url] -[![{{cookiecutter.license}} License][license-shield]][license-url] -[![Coverage](https://img.shields.io/badge/coverage-{{cookiecutter.minimum_coverage}}%25-brightgreen?style=for-the-badge)](docs/coverage/index.html) +[![MIT License][license-shield]][license-url] +[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=for-the-badge)](docs/coverage/index.html) -> {{cookiecutter.project_short_description}} +[![CI Status](https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/workflows/CI/badge.svg?style=for-the-badge)](https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/actions/workflows/ci.yml) +[![CodeQL](https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/workflows/CodeQL%20Security%20Analysis/badge.svg?style=for-the-badge)](https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/actions/workflows/codeql.yml) +[![Python](https://img.shields.io/badge/python-3.12%20%7C%203.13-blue?style=for-the-badge)](https://www.python.org/downloads/) +[![Code Style](https://img.shields.io/badge/code%20style-ruff-000000.svg?style=for-the-badge)](https://github.com/astral-sh/ruff) +[![Security](https://img.shields.io/badge/security-ruff%20%2B%20CodeQL-green?style=for-the-badge)](https://docs.astral.sh/ruff/rules/#flake8-bandit-s) + +> {{PROJECT_DESCRIPTION}} **AI-Enhanced Python Project** built with enterprise-grade architecture, TDD workflows, and zero-config quality standards. @@ -17,8 +23,8 @@ ```bash # Clone and setup -git clone https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}} -cd {{cookiecutter.project_slug}} +git clone https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}} +cd {{PROJECT_NAME}} # Install UV package manager (if not installed) curl -LsSf https://astral.sh/uv/install.sh | sh @@ -75,7 +81,7 @@ Complex projects are developed across multiple AI sessions. `TODO.md` at the roo - **๐ŸŽฏ SOLID Principles** - Single responsibility, dependency inversion, clean interfaces - **๐Ÿ”ง Object Calisthenics** - No primitives, small classes, behavior-rich objects -- **๐Ÿงช TDD Testing** - {{cookiecutter.minimum_coverage}}% coverage requirement with property-based tests +- **๐Ÿงช TDD Testing** - 100% coverage requirement with property-based tests - **โšก Modern Toolchain** - UV, Ruff, PyTest, Hypothesis, PyRight - **๐Ÿš€ Smart Releases** - Calver versioning with AI-generated themed names @@ -96,31 +102,50 @@ task test-report # Detailed coverage report task mut-report # Mutation testing (optional) ``` -## ๐ŸŽฏ Project Structure +## ๐Ÿณ Docker Usage + +Modern Docker setup with multi-stage builds, distroless production images, and comprehensive development workflows. + +### Development Environment + +```bash +# Start development environment with hot reload +docker-compose up +# Run specific services +docker-compose up app # Main application +docker-compose up docs # Documentation server (localhost:8080) + +# Development with profiles +docker-compose --profile test up # Run test suite +docker-compose --profile quality up # Code quality checks ``` -{{cookiecutter.project_slug}}/ -โ”œโ”€โ”€ {{cookiecutter.package_name}}/ # Main application package -โ”‚ โ”œโ”€โ”€ __init__.py # Package initialization -โ”‚ โ””โ”€โ”€ {{cookiecutter.module_name}}.py # Core module -โ”œโ”€โ”€ .opencode/ # AI development agents -โ”‚ โ”œโ”€โ”€ agents/ # Specialized AI agents -โ”‚ โ”‚ โ”œโ”€โ”€ developer.md # 7-phase development workflow -โ”‚ โ”‚ โ”œโ”€โ”€ architect.md # SOLID architecture review -โ”‚ โ”‚ โ””โ”€โ”€ repo-manager.md # Release and PR management -โ”‚ โ””โ”€โ”€ skills/ # Development skills -โ”‚ โ”œโ”€โ”€ session-workflow/ # Multi-session development state -โ”‚ โ”œโ”€โ”€ feature-definition/ # Requirements planning -โ”‚ โ”œโ”€โ”€ tdd/ # Test-driven development -โ”‚ โ”œโ”€โ”€ implementation/ # Guided implementation -โ”‚ โ””โ”€โ”€ code-quality/ # Quality enforcement -โ”œโ”€โ”€ tests/ # Comprehensive test suite -โ”œโ”€โ”€ docs/ # Documentation (api/, tests/, coverage/) -โ”œโ”€โ”€ TODO.md # Development roadmap & session state -โ”œโ”€โ”€ Dockerfile # Multi-stage container build -โ””โ”€โ”€ pyproject.toml # Project configuration + +### Production Deployment + +```bash +# Build production image (distroless, security-optimized) +docker build --target production -t {{PROJECT_NAME}}:prod . + +# Production testing environment +docker-compose -f docker-compose.prod.yml up + +# Security scanning +docker-compose -f docker-compose.prod.yml --profile security up + +# Load testing +docker-compose -f docker-compose.prod.yml --profile load-test up ``` +### Key Features + +- **๐Ÿ”’ Security-First**: Distroless production images, non-root user, vulnerability scanning +- **โšก Performance**: BuildKit caching, uv package manager, optimized layer ordering +- **๐Ÿ“Š Monitoring**: Health checks, resource limits, comprehensive logging +- **๐Ÿ› ๏ธ Development**: Hot reload, separate services for testing/docs/quality checks + + + ## ๐Ÿ”ง Technology Stack | Category | Tools | @@ -130,11 +155,11 @@ task mut-report # Mutation testing (optional) | **Testing** | PyTest + Hypothesis (property-based testing), pytest-html-plus (BDD reports) | | **AI Integration** | OpenCode agents for development automation | | **Documentation** | pdoc with search functionality | -| **Containerization** | Docker with optimized multi-stage builds | +| **Containerization** | Docker with distroless production, BuildKit caching, security scanning | ## ๐Ÿ“ˆ Quality Metrics -- โœ… **{{cookiecutter.minimum_coverage}}% Test Coverage** - Comprehensive test suite including edge cases +- โœ… **100% Test Coverage** - Comprehensive test suite including edge cases - โœ… **Static Type Safety** - Full type hints with protocol-based interfaces - โœ… **Zero Linting Issues** - Automated formatting and style enforcement - โœ… **Property-Based Testing** - Hypothesis for robust validation @@ -143,9 +168,12 @@ task mut-report # Mutation testing (optional) ## ๐Ÿš€ Deployment Ready ```bash -# Production container build -docker build --target prod -t {{cookiecutter.package_name}}:latest . -docker run {{cookiecutter.package_name}}:latest +# Production container (distroless, security-hardened) +docker build --target production -t {{PROJECT_NAME}}:latest . +docker run {{PROJECT_NAME}}:latest + +# Production environment testing +docker-compose -f docker-compose.prod.yml up # Build API documentation task doc-build # Generates docs/api/index.html @@ -174,7 +202,7 @@ Built with AI-assisted development workflows: ## ๐Ÿ“„ License -Distributed under the {{cookiecutter.license}} License. See [LICENSE](LICENSE) for details. +Distributed under the MIT License. See [LICENSE](LICENSE) for details. ## ๐Ÿ™ Built With @@ -185,18 +213,18 @@ Distributed under the {{cookiecutter.license}} License. See [LICENSE](LICENSE) f --- -**Author:** {{cookiecutter.full_name}} ([@{{cookiecutter.github_username}}](https://github.com/{{cookiecutter.github_username}})) -**Project:** [{{cookiecutter.project_slug}}](https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}) -**Documentation:** [{{cookiecutter.github_username}}.github.io/{{cookiecutter.project_slug}}](https://{{cookiecutter.github_username}}.github.io/{{cookiecutter.project_slug}}) +**Author:** {{AUTHOR_NAME}} ([@{{GITHUB_USERNAME}}](https://github.com/{{GITHUB_USERNAME}})) +**Project:** [{{PROJECT_NAME}}](https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}) +**Documentation:** [{{GITHUB_USERNAME}}.github.io/{{PROJECT_NAME}}](https://{{GITHUB_USERNAME}}.github.io/{{PROJECT_NAME}}) -[contributors-shield]: https://img.shields.io/github/contributors/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}.svg?style=for-the-badge -[contributors-url]: https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}.svg?style=for-the-badge -[forks-url]: https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}/network/members -[stars-shield]: https://img.shields.io/github/stars/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}.svg?style=for-the-badge -[stars-url]: https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}/stargazers -[issues-shield]: https://img.shields.io/github/issues/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}.svg?style=for-the-badge -[issues-url]: https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}/issues -[license-shield]: https://img.shields.io/badge/license-{{cookiecutter.license}}-green?style=for-the-badge -[license-url]: https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}/blob/main/LICENSE \ No newline at end of file +[contributors-shield]: https://img.shields.io/github/contributors/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}.svg?style=for-the-badge +[contributors-url]: https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}.svg?style=for-the-badge +[forks-url]: https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/network/members +[stars-shield]: https://img.shields.io/github/stars/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}.svg?style=for-the-badge +[stars-url]: https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/stargazers +[issues-shield]: https://img.shields.io/github/issues/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}.svg?style=for-the-badge +[issues-url]: https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/issues +[license-shield]: https://img.shields.io/badge/license-MIT-green?style=for-the-badge +[license-url]: https://github.com/{{GITHUB_USERNAME}}/{{PROJECT_NAME}}/blob/main/LICENSE \ No newline at end of file diff --git a/.opencode/templates/TODO.md.template b/.opencode/templates/TODO.md.template new file mode 100644 index 0000000..ba62283 --- /dev/null +++ b/.opencode/templates/TODO.md.template @@ -0,0 +1,84 @@ +# Python Project Template - Development TODO + +This file tracks current feature development within epics. For epic/feature tracking, see EPICS.md. +Each session should read both TODO.md and EPICS.md to understand current state. + +**Convention:** `[ ]` = pending, `[x]` = done, `[~]` = in progress, `[-]` = skipped + +> **For AI agents:** Use `/skill session-workflow` and `/skill epic-workflow` for proper workflow management. + +--- + +## Current Epic: Project Foundation +## Current Feature: Project Setup + +### Phase 0: Initial Setup +- [x] Project created via cookiecutter template +- [ ] Review and update `README.md` with project-specific description +- [ ] Install dependencies: `uv venv && uv pip install -e '.[dev]'` +- [ ] Verify base tests pass: `task test` +- [ ] Initialize EPICS.md with first business epic + +### QA Checkpoint +- [ ] @overseer: Review project setup completeness +- [ ] QA Status: โธ๏ธ Pending + +--- + +## Feature Development Phases (Template) + +When starting a new feature, copy these phases: + +### Phase 1: Requirements Gathering +- [ ] @requirements-gatherer: Conduct stakeholder interview +- [ ] Create feature analysis document +- [ ] Define acceptance criteria +- [ ] QA: @overseer reviews requirements + +### Phase 2: Feature Definition +- [ ] @developer /skill feature-definition +- [ ] Document technical requirements +- [ ] Update EPICS.md with feature details + +### Phase 3: Test Development +- [ ] @developer /skill prototype-script (if needed) +- [ ] @developer /skill tdd +- [ ] Write BDD-style tests with Given/When/Then +- [ ] QA: @overseer reviews test quality + +### Phase 4: Design & Architecture +- [ ] @developer /skill signature-design +- [ ] @architect: Review and approve design +- [ ] Address architectural feedback + +### Phase 5: Implementation +- [ ] @developer /skill implementation +- [ ] Implement using TDD (Red-Green-Refactor) +- [ ] QA: @overseer reviews SOLID/DRY/KISS compliance + +### Phase 6: Final Quality Assurance +- [ ] @developer /skill code-quality +- [ ] Run all quality checks (lint, type-check, test) +- [ ] QA: @overseer final approval + +### Phase 7: Feature Completion +- [ ] Update EPICS.md - mark feature complete +- [ ] @developer /skill epic-workflow next-feature +- [ ] Proceed to next feature or close epic + +--- + +## Session Log + +| Date | Session Summary | +|------------|----------------------------------------------------| +| (date) | Project scaffolded via cookiecutter, TODO created | + +--- + +## Notes for Next Session + +- Start with **Phase 1**: update `README.md` with project-specific content +- Then proceed to **Phase 2**: define the core features +- Run `task test` to verify the base template tests pass before making changes +- See `AGENTS.md` for project details and available commands diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/.opencode/templates/pyproject.toml.template similarity index 70% rename from {{cookiecutter.project_slug}}/pyproject.toml rename to .opencode/templates/pyproject.toml.template index 4489d24..692a4b5 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/.opencode/templates/pyproject.toml.template @@ -1,15 +1,15 @@ [project] -name = "{{cookiecutter.project_slug}}" -version = "{{cookiecutter.version}}" -description = "{{cookiecutter.project_short_description}}" +name = "python-project-template" +version = "0.1.20260411" +description = "Python template with some awesome tools to quickstart any Python project" readme = "README.md" requires-python = ">=3.13" license = { file = "LICENSE" } authors = [ - { name = "{{cookiecutter.full_name}}", email = "{{cookiecutter.email}}" } + { name = "eol", email = "nullhack@users.noreply.github.com" } ] maintainers = [ - { name = "{{cookiecutter.full_name}}", email = "{{cookiecutter.email}}" } + { name = "eol", email = "nullhack@users.noreply.github.com" } ] dependencies = [ "dotenv>=0.9.9", @@ -17,25 +17,25 @@ dependencies = [ ] [project.urls] -Repository = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}" -Documentation = "https://github.com/{{cookiecutter.github_username}}/{{cookiecutter.project_slug}}/tree/main/docs/api/" +Repository = "https://github.com/nullhack/python-project-template" +Documentation = "https://github.com/nullhack/python-project-template/tree/main/docs/api/" [project.optional-dependencies] dev = [ "pdoc>=14.0", "pytest>=8.3.5", "pytest-cov>=6.1.1", - "pytest-html-plus>=0.5", + "pytest-html>=4.1.1", "pytest-mock>=3.14.0", "ruff>=0.11.5", "taskipy>=1.14.1", "hypothesis>=6.148.4", - "cosmic-ray>=8.4.3", + "mutmut>=2.5.2", "pyright>=1.1.407", ] [tool.setuptools] -packages = ["{{cookiecutter.package_name}}"] +packages = ["python_package_template"] [tool.ruff.lint] ignore = [] @@ -74,7 +74,7 @@ mccabe.max-complexity = 10 pydocstyle.convention = "google" [tool.ruff.lint.per-file-ignores] -"tests/**" = ["S101", "ANN", "D205", "D212", "D415"] +"tests/**" = ["S101", "ANN", "D205", "D212", "D415", "D100", "D103"] [tool.pytest.ini_options] minversion = "6.0" @@ -96,12 +96,11 @@ addopts = """ --color=yes \ --tb=short \ -q \ ---html-output=docs/tests \ ---json-report=final_report.json \ +--html=docs/tests/report.html \ """ testpaths = [ "tests", - "{{cookiecutter.package_name}}" + "python_package_template" ] python_files = ["*_test.py"] python_functions = ["test_*"] @@ -120,15 +119,15 @@ exclude_lines = [ ] [tool.taskipy.tasks] -run = "python -m {{cookiecutter.package_name}}.{{cookiecutter.module_name}}" +run = "python -m python_package_template.python_module_template" test-report = """\ pytest \ --doctest-modules \ --cov-config=pyproject.toml \ --cov-report html:docs/coverage \ --cov-report term:skip-covered \ - --cov={{cookiecutter.package_name}} \ - --cov-fail-under={{cookiecutter.minimum_coverage}} \ + --cov=python_package_template \ + --cov-fail-under=100 \ --hypothesis-show-statistics \ """ test = """\ @@ -149,8 +148,8 @@ pytest -m slow \ ruff-check = "ruff check . --fix" ruff-format = "ruff format ." lint = "task ruff-check && task ruff-format" -doc-serve = "pdoc ./{{cookiecutter.package_name}} --host localhost --port 8080" -doc-build = "pdoc ./{{cookiecutter.package_name}} -o docs/api --search" +doc-serve = "pdoc ./python_package_template --host localhost --port 8080" +doc-build = "pdoc ./python_package_template -o docs/api --search" doc-publish = """\ task doc-build && \ git checkout gh-pages 2>/dev/null || git checkout -b gh-pages && \ @@ -161,13 +160,12 @@ git add -A && \ git commit -m "Publish API documentation" && \ git push origin gh-pages --force && \ git checkout main""" -mut-report = """ - uv run cosmic-ray new-config mut.toml && \ - uv run cosmic-ray init mut.toml mut.sqlite && \ - uv run cosmic-ray --verbosity=INFO baseline mut.toml && \ - uv run cosmic-ray exec mut.toml mut.sqlite && \ - uv run cr-html mut.sqlite > docs/mut_report.html && \ - rm mut.toml && \ - rm mut.sqlite -""" +mut = "mutmut run" +mut-clean = "mutmut reset && rm -rf docs/mutation/" static-check = "pyright" + +[dependency-groups] +dev = [ + "detect-secrets>=1.5.0", + "safety>=3.7.0", +] diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..3b1f537 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,137 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + } + ], + "results": { + ".opencode/skills/feature-definition/SKILL.md": [ + { + "type": "Secret Keyword", + "filename": ".opencode/skills/feature-definition/SKILL.md", + "hashed_secret": "e5e9fa1ba31ecd1ae84f75caaa474f3a663f05f4", + "is_verified": false, + "line_number": 78 + } + ] + }, + "generated_at": "2026-04-11T17:12:56Z" +} diff --git a/AGENTS.md b/AGENTS.md index 8c585bf..515c09e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,265 +1,274 @@ -# Python Project Template - Enterprise Development Framework +# Python Project Template -This repository contains an enterprise-grade Cookiecutter template for Python projects with integrated AI-enhanced development workflows. +Python template with some awesome tools to quickstart any Python project -## Template Overview +## Session-Based Development -| Component | Description | -|-----------|-------------| -| **Purpose** | Generate Python projects with enterprise development practices and AI-powered workflows | -| **Methodology** | Test-Driven Development (TDD) with mandatory quality gates | -| **Standards** | SOLID principles, Object Calisthenics, DRY/KISS, 100% test coverage | -| **Versioning** | Semantic versioning for template, hybrid calver for generated projects | -| **Architecture** | Domain-driven design with architectural review process | +This project uses a **session workflow** that allows complex development to span multiple AI sessions. Any AI agent can continue work from where the last session stopped. -## Template Management Team +### How it works -### DevOps and Release Engineering +1. **`TODO.md`** at the project root is the shared state between sessions +2. Every session starts by reading `TODO.md` to find the current phase +3. Every session ends by updating `TODO.md` with progress and handoff notes +4. This makes the project AI-agnostic: any agent, any time can continue -- **template-manager**: DevOps Engineer specializing in template lifecycle management, CI/CD pipelines, and quality assurance -- **repo-manager**: Release Engineer managing version control, pull requests, and semantic releases +### Starting a new session +```bash +# The developer agent reads TODO.md automatically +@developer /skill session-workflow +``` + +## Available Skills + +This project includes custom skills for OpenCode: + +### Session Management +- **session-workflow**: Manage multi-session development - read TODO.md, continue from last checkpoint, update progress and hand off cleanly +- **epic-workflow**: Manage epic-based development with automatic feature progression and mandatory QA gates + +### Development Workflow +- **feature-definition**: Define features with SOLID principles and clear requirements +- **prototype-script**: Create quick validation scripts with real data capture +- **tdd**: Write comprehensive tests using TDD with pytest/hypothesis โ€” includes decision guide for when to use plain TDD, Hypothesis (property-based), or Hypothesis stateful testing +- **signature-design**: Design modern Python interfaces with protocols and type hints +- **implementation**: Implement using TDD methodology with real prototype data +- **code-quality**: Enforce quality with ruff, coverage, hypothesis, and cosmic-ray mutation testing + +### Repository Management +- **git-release**: Create semantic releases with hybrid major.minor.calver versioning and themed naming +- **pr-management**: Create and manage pull requests with proper formatting and workflow integration + +### Meta Skills +- **create-skill**: Creates new OpenCode skills following the skill definition standard +- **create-agent**: Creates new OpenCode subagents following the agent definition standard + +## Available Agents + +- **developer**: Main development agent with complete 7-phase TDD workflow and QA integration +- **architect**: Software architect for design review, pattern selection, and SOLID compliance +- **requirements-gatherer**: Business analyst for requirements elicitation and feature analysis +- **overseer**: Quality assurance specialist enforcing standards at mandatory checkpoints +- **repo-manager**: Repository management for Git operations, PRs, commits, and releases + +## Development Commands + +```bash +# Install dependencies +uv venv +uv pip install '.[dev]' -### Available Skills +# Run the application +task run -- **template-test**: Test cookiecutter template generation with various configurations -- **template-release**: Manage template releases with semantic versioning -- **git-release**: Create semantic releases for the template repository -- **pr-management**: Create and manage pull requests for template improvements +# Run tests (full suite with coverage report) +task test -## Template Structure +# Run fast tests only (skip slow tests) +task test-fast +# Run slow tests only +task test-slow + +# Run linting +task lint + +# Run type checking +task static-check + +# Serve documentation +task doc-serve + +# Build documentation +task doc-build ``` -python-project-template/ -โ”œโ”€โ”€ cookiecutter.json # Template configuration -โ”œโ”€โ”€ {{cookiecutter.project_slug}}/ # Generated project template -โ”‚ โ”œโ”€โ”€ .opencode/ # AI agents for generated projects -โ”‚ โ”‚ โ”œโ”€โ”€ agents/ -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ developer.md # Main development agent -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ architect.md # Design review agent -<<<<<<< HEAD -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ requirements-gatherer.md # Business analyst agent -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ overseer.md # QA specialist agent -======= -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ overseer.md # Quality assurance agent -โ”‚ โ”‚ โ”‚ โ”œโ”€โ”€ requirements-gatherer.md # Requirements gathering agent ->>>>>>> origin/main -โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ repo-manager.md # Repository management agent -โ”‚ โ”‚ โ””โ”€โ”€ skills/ -โ”‚ โ”‚ โ”œโ”€โ”€ session-workflow/ # Session state management -โ”‚ โ”‚ โ”œโ”€โ”€ epic-workflow/ # Epic-based development -โ”‚ โ”‚ โ”œโ”€โ”€ feature-definition/ # SOLID feature planning -โ”‚ โ”‚ โ”œโ”€โ”€ prototype-script/ # Quick validation scripts -โ”‚ โ”‚ โ”œโ”€โ”€ tdd/ # Test-driven development -โ”‚ โ”‚ โ”œโ”€โ”€ signature-design/ # Interface design -โ”‚ โ”‚ โ”œโ”€โ”€ implementation/ # TDD implementation -โ”‚ โ”‚ โ”œโ”€โ”€ code-quality/ # Quality enforcement -โ”‚ โ”‚ โ”œโ”€โ”€ git-release/ # Release management -โ”‚ โ”‚ โ”œโ”€โ”€ pr-management/ # Pull request workflows -โ”‚ โ”‚ โ”œโ”€โ”€ create-skill/ # Create new skills -โ”‚ โ”‚ โ””โ”€โ”€ create-agent/ # Create new agents -โ”‚ โ”œโ”€โ”€ pyproject.toml # Project configuration -โ”‚ โ””โ”€โ”€ AGENTS.md # Generated project AI documentation -โ”œโ”€โ”€ .opencode/ # Meta agents for template itself -โ”‚ โ”œโ”€โ”€ agents/ -โ”‚ โ”‚ โ”œโ”€โ”€ template-manager.md # Template development and management -โ”‚ โ”‚ โ””โ”€โ”€ repo-manager.md # Template repository operations -โ”‚ โ””โ”€โ”€ skills/ -โ”‚ โ”œโ”€โ”€ template-test/ # Template testing -โ”‚ โ”œโ”€โ”€ template-release/ # Template release management -โ”‚ โ”œโ”€โ”€ git-release/ # Semantic releases for template -โ”‚ โ””โ”€โ”€ pr-management/ # Pull request workflows for template -โ””โ”€โ”€ docs/ # Template documentation + +## Docker Commands + +```bash +# Development with Docker +docker-compose up # Start development environment +docker-compose --profile test up # Run test suite +docker-compose --profile quality up # Code quality checks + +# Production +docker build --target production -t app:prod . # Build production image +docker-compose -f docker-compose.prod.yml up # Production testing +docker-compose -f docker-compose.prod.yml --profile security up # Security scan ``` -## What Generated Projects Get - -When developers use this template, they get: - -### Epic-Based Development with QA Gates -1. **Requirements Gathering** โ†’ Business analyst interviews and analysis -2. **QA Checkpoint** โ†’ Requirements completeness review -3. **Test-Driven Development** โ†’ BDD tests with pytest/hypothesis -4. **QA Checkpoint** โ†’ Test quality review -5. **Design & Architecture** โ†’ Pattern selection and SOLID design -6. **Implementation** โ†’ TDD methodology (Red-Green-Refactor) -7. **QA Checkpoint** โ†’ SOLID/DRY/KISS compliance review -8. **Final Quality** โ†’ Comprehensive quality checks -9. **QA Checkpoint** โ†’ Final approval before feature completion -10. **Automatic Progression** โ†’ System moves to next feature in epic - -### AI Agents with Industry Roles -- **@developer** โ†’ Development lead with TDD workflow and QA integration -- **@architect** โ†’ Software architect for design patterns and SOLID principles -- **@requirements-gatherer** โ†’ Business analyst using BABOK principles -- **@overseer** โ†’ QA specialist with mandatory quality checkpoints -- **@repo-manager** โ†’ Release engineer for repository operations +## Documentation + +This project uses **pdoc** for API documentation generation: -### Repository Management -- Hybrid versioning: `v1.2.20260302` (major.minor.calver) -- AI-generated themed releases (unique per release): "Blooming Narwhal", "Crystal Jellyfish", "Electric Firefly" -- Automated PR workflows with conventional commits -- GitHub CLI integration - -### Code Quality Standards -- SOLID principles enforcement with architect review -- DRY/KISS principles with overseer validation -- Object calisthenics compliance (9 rules) -- 100% test coverage requirement -- Comprehensive linting with ruff -- Static type checking with pyright -- Property-based testing with Hypothesis -- API documentation with pdoc -- BDD-style test reports with pytest-html-plus -- Mandatory QA gates that cannot be bypassed - -## Template Usage - -### Creating a New Project ```bash -# Use the latest template -cookiecutter gh:your-username/python-project-template +# Serve documentation locally +task doc-serve -# Or use a specific version -cookiecutter gh:your-username/python-project-template --checkout v1.2.20260312 +# Build static documentation with search +task doc-build ``` -### Template Development Workflow -```bash -# 1. Make changes to template -# Edit template files, add new agents/skills +Generated docs are in `docs/api/` - open `docs/api/index.html` to browse. + +## Test Conventions + +This project uses BDD-style tests with the following conventions: + +### Test File Naming -# 2. Test template generation -@template-manager /skill template-test +Test filenames should follow _test.py -# 3. Create template release -@template-manager /skill template-release +### Test Function Naming +```python +# Format: test__should_ +def test__should_(): ... +``` -# 4. Generated projects now have new features +### BDD Docstrings +All test functions must have Given/When/Then docstrings: +```python +def test_federation_created_should_have_active_status(): + """ + Given: A valid federation with required fields + When: Federation is created + Then: Status should be active + """ ``` -## Template Versioning - -### Hybrid Calver Versioning for Template -- **Major (v2.x.20260401)**: Breaking changes to cookiecutter variables -- **Minor (v1.x.20260315)**: New agents, skills, workflow features, or same-day releases - -### Recent Releases -- **v1.0.20260312**: Initial release with development workflow -- **v1.1.20260312**: Added repository management agent -- **v1.2.20260312**: Added meta template management system -- **v1.3.20260313**: Added session-workflow skill -- **v1.4.20260313**: Added AI-driven themed naming -- **v1.5.20260403**: Replaced mkdocs with pdoc for API docs, added pytest-html-plus with BDD docstring display -- **v1.6.20260410**: Added QA-gated epic workflow with business analyst and QA specialist agents - -## Generated Project Features - -### Agents Included in Generated Projects -<<<<<<< HEAD -- **@developer**: Complete development workflow with mandatory QA gates -- **@architect**: Software architect for design patterns and SOLID principles -- **@requirements-gatherer**: Business analyst for stakeholder requirements -- **@overseer**: QA specialist enforcing quality at checkpoints -======= -- **@developer**: Complete 7-phase development workflow -- **@architect**: Design review and SOLID principles enforcement -- **@overseer**: Quality assurance - reviews work after each test implementation -- **@requirements-gatherer**: Gathers requirements, updates docs, creates analysis for architect ->>>>>>> origin/main -- **@repo-manager**: Git operations, PRs, and themed releases - -### Skills Included in Generated Projects -- **session-workflow**, **epic-workflow** (multi-session and epic management) -- **feature-definition**, **prototype-script**, **tdd** -- **signature-design**, **implementation**, **code-quality** -- **git-release**, **pr-management** -- **create-skill**, **create-agent** - -### Example Generated Project Usage +### Running Tests + ```bash -<<<<<<< HEAD -# In a generated project - Epic-based workflow -@requirements-gatherer # Gather requirements -@overseer # QA: Requirements review -@developer /skill tdd # Write tests -@overseer # QA: Test quality review -@architect # Design approval -@developer /skill implementation # Implement feature -@overseer # QA: Code quality review -@developer /skill epic-workflow next-feature # Auto-progress to next -======= -# In a generated project - -# 0. Start by gathering requirements (new projects) -@requirements-gatherer # Ask questions, create analysis, update docs -@architect # Review analysis and approve design +# Run fast tests (skip slow tests) +task test-fast -# 1. Define and implement a feature -@developer /skill feature-definition # Define new feature -@developer /skill prototype-script # Create prototype -@developer /skill tdd # Write tests -@overseer # Review tests - request changes if needed -@developer /skill signature-design # Design interfaces -@architect # Get design approval -@developer /skill implementation # Implement feature -@developer /skill code-quality # Run quality checks -@overseer # Final review - -# 2. Manage repository -@repo-manager /skill pr-management # Create PR -@repo-manager /skill git-release # Create release ->>>>>>> origin/main +# Run only slow tests +task test-slow + +# Full test suite with coverage +task test ``` -## Template Development +### Checking Test Compliance +- **pytest-html report**: `docs/tests/report.html` - BDD docstrings displayed +- **Coverage report**: `docs/coverage/index.html` - View coverage by file + +## Code Quality Standards + +- **Linting**: ruff with Google style conventions (D205, D212, D415 disabled for test files to allow BDD docstrings) +- **Type Checking**: pyright +- **Test Coverage**: Minimum 100% +- **Python Version**: >=3.13 +- **Test Markers**: `slow` marks tests >50ms (SQLite, Hypothesis, web routes) -### Making Template Changes -1. **Update Template Files**: Modify files in `{{cookiecutter.project_slug}}/` -2. **Add New Agents/Skills**: Create in `.opencode/` directory -3. **Test Changes**: Use `@template-manager /skill template-test` -4. **Release Template**: Use `@template-manager /skill template-release` +## Release Management -### Quality Standards for Template -- All generated projects must pass quality checks -- Template testing with multiple configurations -- Documentation must be up-to-date -- YAML frontmatter must be valid in all agents/skills +This project uses a hybrid versioning system: `v{major}.{minor}.{YYYYMMDD}` -### Contributing to Template -1. Fork the template repository -2. Make improvements following template standards -3. Test thoroughly with various configurations -4. Create PR with description of changes -5. Template maintainers will review and merge +### Version Examples +- `v1.2.20260302` - Version 1.2, release on March 2, 2026 +- `v1.3.20260313` - Version 1.3, release on March 13, 2026 +- `v1.4.20260313` - Version 1.4, second release same day (increment minor) -## Template Management Workflow +### Release Naming +Releases use AI-generated adjective-animal names. Each release gets a unique name based on PR content. Examples: +- `v1.5.20260403 - Crystal Jellyfish` (documentation overhaul) +- `v1.6.20260404 - Velvet Manta` (refactoring) +- `v1.7.20260405 - Electric Firefly` (performance) -### Agent Roles in Template Development -- **@template-manager**: Handles template-specific tasks like testing generation, validating cookiecutter variables, and releasing new template versions -- **@repo-manager**: Manages the template repository itself - creating PRs, commits, GitHub releases, and handling version control +The AI analyzes PR content and generates creative, unique names containing a characteristic/adjectiv and an animal name -### Example Template Development +### Creating Releases +Use the repo-manager agent: ```bash -# Working on template improvements -@template-manager /skill template-test # Test template generation -@repo-manager /skill pr-management # Create PR for changes -@template-manager /skill template-release # Release new template version +@repo-manager /skill git-release ``` -## Integration with OpenCode - -The template is designed to work seamlessly with OpenCode: +## Using OpenCode +Initialize OpenCode in this project: ```bash -# In generated projects opencode /opencode -/init # Generate fresh AGENTS.md +``` + +Then run `/init` to generate a fresh `AGENTS.md` based on your project's current state. + +### Example Workflows -# Use the workflow -@developer # Main development agent -@architect # Architecture review -@repo-manager # Repository operations +#### Starting a new project +```bash +# 1. Start with requirements gathering +@requirements-gatherer # Interview stakeholders, create analysis +@architect # Review requirements and approve approach +@developer /skill epic-workflow start-epic "Core Features" ``` -This template provides a complete AI-enhanced development environment for Python projects, ensuring high code quality, proper testing, and professional repository management. \ No newline at end of file +#### Epic-based feature development with QA gates + +# 0. Gather requirements first (for new projects) +@requirements-gatherer # Ask questions, create analysis, update docs +@architect # Review analysis and approve design +@developer # Start implementation with approved TODO + +# 1. Define and implement a feature +@developer /skill feature-definition +@developer /skill prototype-script +@developer /skill tdd +@overseer # Review tests - request changes if needed +@developer /skill signature-design +@architect # Review design +@developer /skill implementation +@developer /skill code-quality +@overseer # Final review before moving on + +# 1. Requirements & Analysis +@requirements-gatherer # Gather detailed requirements +@overseer # QA checkpoint: requirements review + +# 2. Test Development +@developer /skill tdd # Write BDD tests +@overseer # QA checkpoint: test quality review + +# 3. Design & Architecture +@developer /skill signature-design +@architect # Approve design and patterns + +# 4. Implementation +@developer /skill implementation +@overseer # QA checkpoint: SOLID/DRY/KISS review + +# 5. Final Quality +@developer /skill code-quality +@overseer # QA checkpoint: final approval + +# 6. Feature completion - system auto-progresses to next +@developer /skill epic-workflow next-feature +``` + +#### Creating releases +```bash +# After all epic features complete +@overseer # Final pre-release QA review +@repo-manager /skill pr-management +@repo-manager /skill git-release +``` + +#### Session management +```bash +# Start of session +@developer /skill session-workflow # Read TODO.md, understand state + +# End of session +@developer /skill session-workflow # Update TODO.md, commit changes +``` + +### Quality Assurance Protocol + +**The @overseer agent enforces mandatory QA checkpoints:** +1. After requirements gathering - completeness review +2. After TDD phase - test quality review +3. After implementation - SOLID/DRY/KISS review +4. Before feature completion - final approval + +**Development cannot proceed without @overseer approval at each gate.** diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8a6be45 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,153 @@ +# syntax=docker/dockerfile:1.7 +# Modern Dockerfile for python-project-template +# Features: multi-stage build, distroless prod, security scanning, BuildKit caching + +ARG PYTHON_VERSION=3.13.1 +ARG BUILDPLATFORM=linux/amd64 + +# ============================================================================= +# Base stage: Python + uv package manager +# ============================================================================= +FROM --platform=$BUILDPLATFORM python:${PYTHON_VERSION}-alpine AS base + +# Install uv for ultra-fast Python package management +RUN --mount=type=cache,target=/root/.cache/pip \ + pip install --upgrade pip uv + +# Create non-root user early +RUN addgroup --system --gid 1001 appuser && \ + adduser --system --uid 1001 --ingroup appuser appuser + +WORKDIR /app + +# ============================================================================= +# Dependencies stage: Install and cache Python dependencies +# ============================================================================= +FROM base AS deps + +# Install build dependencies +RUN apk add --no-cache \ + build-base \ + linux-headers \ + git + +# Copy dependency files first (better layer caching) +COPY pyproject.toml ./ + +# Install dependencies with uv (much faster than pip) +RUN --mount=type=cache,target=/root/.cache/uv \ + --mount=type=cache,target=/root/.cache/pip \ + uv pip install --system '.[dev]' taskipy + +# ============================================================================= +# Test stage: Run linting and tests +# ============================================================================= +FROM deps AS test + +# Copy source code +COPY . . + +# Change ownership to appuser +RUN chown -R appuser:appuser /app +USER appuser + +# Set build arguments for conditional testing +ARG TESTBUILD=true +ENV TESTBUILD=$TESTBUILD + +# Run quality checks and tests if enabled +RUN if [ "$TESTBUILD" = "true" ]; then \ + echo "๐Ÿ” Running linting..." && \ + task lint && \ + echo "๐Ÿงช Running tests..." && \ + task test && \ + echo "โœ… All quality checks passed!"; \ + fi + +# ============================================================================= +# Build stage: Create wheel distribution +# ============================================================================= +FROM test AS build + +# Build wheel package +RUN --mount=type=cache,target=/root/.cache/uv \ + uv build --wheel --out-dir dist + +# ============================================================================= +# Security scanning stage (optional but recommended) +# ============================================================================= +FROM aquasec/trivy:latest AS security-scan + +# Copy built artifacts for scanning +COPY --from=build /app/dist /scan/dist +COPY --from=build /app/pyproject.toml /scan/ + +# Run security scan (will fail build on HIGH/CRITICAL vulnerabilities) +RUN trivy fs --exit-code 1 --severity HIGH,CRITICAL /scan || \ + (echo "โŒ Security vulnerabilities found! Check the output above." && exit 1) + +# ============================================================================= +# Runtime preparation: Install wheel in clean Python environment +# ============================================================================= +FROM python:${PYTHON_VERSION}-alpine AS runtime-prep + +# Install the wheel package in a clean environment +COPY --from=build /app/dist/*.whl /tmp/ +RUN pip install --prefix=/app/python /tmp/*.whl + +# ============================================================================= +# Production stage: Minimal distroless runtime +# ============================================================================= +FROM gcr.io/distroless/python3-debian12:latest AS production + +# Copy installed Python packages from runtime prep +COPY --from=runtime-prep /app/python /usr/local + +# Set working directory +WORKDIR /app + +# Use non-root user (distroless default nonroot user) +USER nonroot:nonroot + +# Configure Python for production +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONHASHSEED=random + +# Health check using module execution +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -m python_package_template.python_module_template || exit 1 + +# Default command +CMD ["python", "-m", "python_package_template.python_module_template"] + +# ============================================================================= +# Development stage: For local development with hot reload +# ============================================================================= +FROM deps AS development + +# Install development tools +RUN --mount=type=cache,target=/root/.cache/uv \ + uv pip install --system watchdog + +# Copy source code +COPY . . + +# Change ownership and switch to non-root user +RUN chown -R appuser:appuser /app +USER appuser + +# Expose common development ports +EXPOSE 8000 8080 5678 + +# Development command with auto-reload +CMD ["python", "-m", "python_package_template.python_module_template"] + +# ============================================================================= +# Metadata and labels +# ============================================================================= +LABEL maintainer="eol" +LABEL version="0.1.20260411" +LABEL description="Python project template with modern Docker practices" +LABEL org.opencontainers.image.source="https://github.com/nullhack/python-project-template" +LABEL org.opencontainers.image.documentation="https://github.com/nullhack/python-project-template/tree/main/docs/api/" \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/EPICS.md b/EPICS.md similarity index 97% rename from {{cookiecutter.project_slug}}/EPICS.md rename to EPICS.md index 21b8673..32552a8 100644 --- a/{{cookiecutter.project_slug}}/EPICS.md +++ b/EPICS.md @@ -1,4 +1,4 @@ -# {{cookiecutter.project_name}} - Epic Tracking +# Python Project Template - Epic Tracking This file tracks all epics and their features. Each feature goes through mandatory QA gates before proceeding to the next. diff --git a/LICENSE b/LICENSE index fa5d0cf..9ae9e81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022, eol +Copyright (c) 2026, eol Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -19,3 +19,4 @@ 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. + diff --git a/README.md b/README.md index 9664ac9..d659a40 100644 --- a/README.md +++ b/README.md @@ -1,192 +1,304 @@ -# ๐Ÿš€ AI-Enhanced Python Project Template - -[![Contributors][contributors-shield]][contributors-url] -[![Forks][forks-shield]][forks-url] -[![Stargazers][stars-shield]][stars-url] -[![Issues][issues-shield]][issues-url] -[![MIT License][license-shield]][license-url] - -> **Ship production-ready Python projects faster with AI-powered development workflows** - -### Latest Release: [v1.7.20260410](https://github.com/nullhack/python-project-template/releases/tag/v1.7.20260410) - Vivid Cardinal - -Modern cookiecutter template delivering enterprise-grade Python projects with **OpenCode AI agents**, **TDD/BDD workflows**, and **zero-config quality standards**. - -## โœจ What You Get - -๐Ÿค– **Enterprise AI Team** - 5 specialized agents: Developer, Architect, Business Analyst, QA Specialist, Release Engineer -๐Ÿ—๏ธ **SOLID Architecture** - Object Calisthenics, Dependency Inversion, Protocol-based design with architect review -โšก **Zero-Config Setup** - UV package manager, Ruff formatting, pytest + Hypothesis testing -๐ŸŽฏ **Mandatory QA Gates** - 4 quality checkpoints enforced by QA specialist throughout development -๐Ÿ”„ **Smart Releases** - Hybrid calver versioning with AI-generated themed names -๐Ÿ“‹ **Epic-Based Workflow** - Requirements-driven development with automatic feature progression - -## ๐ŸŽฏ Perfect For - -- **Startups** building MVPs with enterprise standards -- **Teams** needing consistent code quality and architecture -- **Developers** wanting AI-assisted TDD/BDD workflows -- **Projects** requiring rapid iteration with zero technical debt - -## ๐Ÿš€ Quick Start - -### Prerequisites - -Install the required tools: - -```bash -# Install OpenCode AI assistant -curl -fsSL https://opencode.ai/install.sh | sh - -# Install UV package manager (replaces pip/poetry/virtualenv) -curl -LsSf https://astral.sh/uv/install.sh | sh - -# Install Cookiecutter -pip install cookiecutter -``` - -### Create Your Project - -```bash -# Generate your AI-enhanced Python project -cookiecutter gh:nullhack/python-project-template - -# Enter your project directory -cd your-project-name - -# Initialize AI development environment -opencode -/init - -# Start an epic with requirements gathering -@requirements-gatherer # Business analysis -@developer /skill epic-workflow start-epic "MVP Features" -``` - -### Instant Development Ready - -```bash -# Install dependencies and activate virtual environment -uv venv && uv pip install -e '.[dev]' - -# Run the complete development workflow -task test # 100% coverage + property-based tests -task lint # Ruff formatting + static analysis -task doc-serve # Live documentation server - -# Deploy with confidence -@repo-manager /skill git-release -``` - -## ๐Ÿ›๏ธ Architecture & Workflow - -### Epic-Based Development with Mandatory QA Gates - -1. **Requirements Gathering** โ†’ Business analyst conducts stakeholder interviews -2. **QA Gate #1** โ†’ Requirements completeness review by QA specialist -3. **Test-Driven Development** โ†’ BDD-style tests with pytest + Hypothesis -4. **QA Gate #2** โ†’ Test quality and coverage review -5. **Design & Architecture** โ†’ Pattern selection and SOLID design by architect -6. **Implementation** โ†’ TDD methodology with Red-Green-Refactor cycle -7. **QA Gate #3** โ†’ SOLID/DRY/KISS compliance review -8. **Final Quality** โ†’ Comprehensive quality checks -9. **QA Gate #4** โ†’ Final approval before feature completion -10. **Automatic Progression** โ†’ System advances to next feature in epic - -### Smart Release Management - -- **Hybrid Versioning**: `v{major}.{minor}.{YYYYMMDD}` (same-day releases increment minor) -- **Themed Releases**: AI-generated names based on PR sentiment - - Performance: `"Swift Cheetah"` `"Lightning Falcon"` - - Security: `"Vigilant Owl"` `"Guardian Bear"` - - Features: `"Creative Fox"` `"Innovative Dolphin"` - -## ๐Ÿ”ง Included Technology Stack - -**Development** -- Python 3.13+ with modern type hints -- UV for blazing-fast dependency management -- Ruff for linting and formatting (replaces 8+ tools) -- PyTest + Hypothesis for comprehensive testing - -**AI Integration - Your Enterprise Development Team** -- **@developer**: Development Lead implementing TDD workflow with QA integration -- **@architect**: Software Architect ensuring SOLID principles and design patterns -- **@requirements-gatherer**: Business Analyst using BABOK methodology -- **@overseer**: QA Specialist enforcing mandatory quality checkpoints -- **@repo-manager**: Release Engineer handling versioning and deployments - -**Quality Assurance** -- 100% test coverage requirement -- Static type checking with Pyright -- Property-based testing for edge cases -- Mutation testing with Cosmic Ray - -**Documentation & Deployment** -- pdoc for API documentation with search -- pytest-html-plus with BDD docstring display -- Docker containerization -- GitHub Actions CI/CD -- Automated documentation deployment - - -## ๐Ÿ“ˆ Template Roadmap - -- [x] โœจ AI-powered development workflow with OpenCode integration -- [x] ๐Ÿ—๏ธ SOLID architecture enforcement with object calisthenics -- [x] ๐Ÿค– Automated repository management with smart releases -- [x] โšก Modern toolchain (UV, Ruff, PyTest, Hypothesis) -- [x] ๐Ÿ“‹ Epic-based workflow with automatic feature progression -- [x] ๐ŸŽฏ Mandatory QA gates with dedicated QA specialist agent -- [x] ๐Ÿ’ผ Business analyst agent for requirements gathering -- [ ] ๐Ÿ”’ Advanced security scanning and SBOM generation -- [ ] ๐Ÿ“Š Performance benchmarking and optimization workflows - -## ๐Ÿค Contributing - -Contributions make this template better for everyone! We welcome: - -- ๐Ÿ› Bug reports and fixes -- โœจ New agents and skills -- ๐Ÿ“š Documentation improvements -- ๐ŸŽฏ Workflow optimizations - -```bash -# Quick contribution setup -cookiecutter gh:nullhack/python-project-template -cd your-contribution -@developer /skill feature-definition -@repo-manager /skill pr-management -``` - -## ๐Ÿ“„ License - -MIT License - see [`LICENSE`](LICENSE) for details. - -## ๐Ÿ™ Credits - -Built on the shoulders of giants: - -- [OpenCode](https://opencode.ai) - AI-powered development platform -- [UV](https://astral.sh/uv/) - Blazing fast Python package manager -- [Ruff](https://astral.sh/ruff/) - Extremely fast Python linter -- [Hypermodern Python](https://cjolowicz.github.io/posts/hypermodern-python-01-setup/) - Modern Python practices -- [Hypothesis](https://hypothesis.readthedocs.io/) - Property-based testing framework - ---- - -**[โญ Star this repo](https://github.com/nullhack/python-project-template) if it powers your next breakthrough!** - - - - -[contributors-shield]: https://img.shields.io/github/contributors/nullhack/python-project-template.svg?style=for-the-badge -[contributors-url]: https://github.com/nullhack/python-project-template/graphs/contributors -[forks-shield]: https://img.shields.io/github/forks/nullhack/python-project-template.svg?style=for-the-badge -[forks-url]: https://github.com/nullhack/python-project-template/network/members -[stars-shield]: https://img.shields.io/github/stars/nullhack/python-project-template.svg?style=for-the-badge -[stars-url]: https://github.com/nullhack/python-project-template/stargazers -[issues-shield]: https://img.shields.io/github/issues/nullhack/python-project-template.svg?style=for-the-badge -[issues-url]: https://github.com/nullhack/python-project-template/issues -[license-shield]: https://img.shields.io/badge/license-MIT-green?style=for-the-badge -[license-url]: https://github.com/nullhack/python-project-template/blob/main/LICENSE.txt +# ๐Ÿš€ AI-Enhanced Python Project Template + +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![MIT License][license-shield]][license-url] +[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=for-the-badge)](docs/coverage/index.html) + +[![CI Status](https://github.com/nullhack/python-project-template/workflows/CI/badge.svg?style=for-the-badge)](https://github.com/nullhack/python-project-template/actions/workflows/ci.yml) +[![CodeQL](https://github.com/nullhack/python-project-template/workflows/CodeQL%20Security%20Analysis/badge.svg?style=for-the-badge)](https://github.com/nullhack/python-project-template/actions/workflows/codeql.yml) +[![Python](https://img.shields.io/badge/python-3.13-blue?style=for-the-badge)](https://www.python.org/downloads/) +[![Code Style](https://img.shields.io/badge/code%20style-ruff-000000.svg?style=for-the-badge)](https://github.com/astral-sh/ruff) +[![Security](https://img.shields.io/badge/security-ruff%20%2B%20CodeQL-green?style=for-the-badge)](https://docs.astral.sh/ruff/rules/#flake8-bandit-s) + +> **Ship production-ready Python projects faster with AI-powered development workflows and modern containerization** + +### Development Version: [v0.1.20260411](https://github.com/nullhack/python-project-template/releases/tag/v0.1.20260411) - Enhanced Docker Edition + +**Revolutionary Python template** delivering enterprise-grade projects with **OpenCode AI agents**, **distroless Docker containers**, **TDD/BDD workflows**, and **security-first containerization**. + +## โœจ What You Get + +๐Ÿค– **Enterprise AI Development Team** - 5 specialized agents: Developer, Architect, Business Analyst, QA Specialist, Release Engineer +๐Ÿณ **Modern Containerization** - Multi-stage Docker builds with distroless production images and security scanning +๐Ÿ”’ **Security-First Approach** - Non-root containers, vulnerability scanning, and minimal attack surface +โšก **Zero-Config Development** - Hot reload, automated testing, and instant deployment workflows +๐Ÿ—๏ธ **SOLID Architecture** - Object Calisthenics, Dependency Inversion, Protocol-based design with architect review +๐ŸŽฏ **Mandatory QA Gates** - 4 quality checkpoints enforced by QA specialist throughout development +๐Ÿ”„ **Smart Releases** - Hybrid calver versioning with AI-generated themed names +๐Ÿ“‹ **Epic-Based Workflow** - Requirements-driven development with automatic feature progression + +## ๐ŸŽฏ Perfect For + +- **Startups** needing production-ready containers from day one +- **DevOps Teams** requiring secure, optimized Docker workflows +- **Enterprises** demanding zero-compromise security and quality +- **Developers** wanting AI-assisted development with modern tooling +- **Projects** scaling from development to production seamlessly + +## ๐Ÿš€ Quick Start + +### Prerequisites + +Install the essential tools: + +```bash +# Install OpenCode AI assistant +curl -fsSL https://opencode.ai/install.sh | sh + +# Install UV package manager (5-10x faster than pip) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install Docker with BuildKit support +# Follow: https://docs.docker.com/get-docker/ +``` + +### Choose Your Development Style + +#### ๐Ÿณ **Docker-First Development** *(Recommended)* + +```bash +# Clone the template +git clone https://github.com/nullhack/python-project-template.git your-project +cd your-project + +# Start development environment with hot reload +docker-compose up + +# Initialize AI development environment +opencode && /init + +# Start an epic with requirements gathering +@requirements-gatherer # Business analysis and stakeholder interviews +@developer /skill epic-workflow start-epic "MVP Features" +``` + +#### โšก **Native Development** + +```bash +# Clone and setup locally +git clone https://github.com/nullhack/python-project-template.git your-project +cd your-project + +# Setup development environment +uv venv && uv pip install -e '.[dev]' + +# Validate everything works +task test && task lint && task static-check + +# Initialize AI development +opencode && /init +``` + +## ๐Ÿณ Modern Docker Workflows + +### Development Environment + +```bash +# Full development stack with hot reload +docker-compose up + +# Specific development services +docker-compose up app # Main application +docker-compose up docs # Documentation server (localhost:8080) + +# Quality assurance workflows +docker-compose --profile test up # Complete test suite +docker-compose --profile quality up # Linting and type checking +``` + +### Production Deployment + +```bash +# Build security-hardened production image +docker build --target production -t your-project:prod . + +# Production testing environment +docker-compose -f docker-compose.prod.yml up + +# Security and performance validation +docker-compose -f docker-compose.prod.yml --profile security up # Vulnerability scanning +docker-compose -f docker-compose.prod.yml --profile load-test up # Load testing +``` + +### Container Security Features + +- **๐Ÿ”’ Distroless Production Images** - Minimal attack surface, no shell access +- **๐Ÿ‘ค Non-Root Execution** - Enhanced security throughout all container stages +- **๐Ÿ›ก๏ธ Vulnerability Scanning** - Automated Trivy security scanning in CI/CD +- **๐Ÿ“Š Resource Limits** - Production-ready CPU and memory constraints +- **๐Ÿšซ Read-Only Filesystem** - Immutable production containers + +## ๐Ÿ›๏ธ Architecture & Workflow + +### Epic-Based Development with Mandatory QA Gates + +1. **Requirements Gathering** โ†’ `@requirements-gatherer` conducts stakeholder interviews using BABOK methodology +2. **๐Ÿ”’ QA Gate #1** โ†’ `@overseer` enforces requirements completeness review +3. **Test-Driven Development** โ†’ `@developer /skill tdd` creates BDD-style tests with pytest + Hypothesis +4. **๐Ÿ”’ QA Gate #2** โ†’ `@overseer` reviews test quality and coverage standards +5. **Design & Architecture** โ†’ `@architect` ensures SOLID patterns and design review +6. **Implementation** โ†’ `@developer /skill implementation` follows TDD Red-Green-Refactor cycle +7. **๐Ÿ”’ QA Gate #3** โ†’ `@overseer` validates SOLID/DRY/KISS compliance +8. **Final Quality** โ†’ `@developer /skill code-quality` runs comprehensive quality checks +9. **๐Ÿ”’ QA Gate #4** โ†’ `@overseer` provides final approval before feature completion +10. **Automatic Progression** โ†’ System advances to next feature in epic + +**๐Ÿšซ Development cannot proceed without @overseer approval at each mandatory gate** + +### Multi-Session Development State + +Complex projects span multiple AI sessions using shared state management: + +```bash +# Start any session: read TODO.md, understand current state, continue work +@developer /skill session-workflow + +# End any session: update TODO.md, commit progress, hand off to next session +@developer /skill session-workflow +``` + +## ๐Ÿ”ง Technology Stack + +**๐Ÿณ Containerization** +- Docker multi-stage builds with BuildKit optimization +- Distroless production images (gcr.io/distroless/python3) +- Security scanning with Trivy integration +- Hot reload development containers + +**๐Ÿค– AI Development Team** +- **@developer**: TDD workflow implementation with QA integration +- **@architect**: SOLID principles and design pattern enforcement +- **@requirements-gatherer**: Business analysis using BABOK methodology +- **@overseer**: Quality gates and mandatory checkpoint enforcement +- **@repo-manager**: Release management and deployment workflows + +**โšก Modern Python Stack** +- Python 3.13+ with advanced type hints and protocols +- UV package manager (5-10x faster dependency management) +- Ruff formatting and linting (replaces 8+ tools) +- PyTest + Hypothesis for comprehensive testing + +**๐Ÿ“Š Quality Assurance** +- 100% test coverage requirement with branch coverage +- Property-based testing for edge case discovery +- Static type checking with Pyright +- Mutation testing with mutmut for test quality validation + +## ๐Ÿ“‹ Development Commands + +### Native Development + +```bash +# Core workflow +task run # Execute main application +task test # Complete test suite with coverage +task test-fast # Fast tests (skip slow integration tests) +task lint # Ruff formatting and linting +task static-check # Pyright type checking +task doc-serve # Live documentation server +task doc-build # Static API documentation generation +``` + +### Docker Development + +```bash +# Development workflows +docker-compose up # Hot reload development +docker-compose --profile test up # Complete test suite +docker-compose --profile quality up # Code quality pipeline + +# Production workflows +docker build --target production -t app:prod . # Security-optimized build +docker-compose -f docker-compose.prod.yml up # Production testing +docker-compose -f docker-compose.prod.yml --profile security up # Vulnerability scan +``` + +## ๐Ÿ“ˆ Quality Metrics & Standards + +- โœ… **100% Test Coverage** - Branch and line coverage with pytest-cov +- โœ… **Security Hardened** - Distroless containers, non-root execution, vulnerability scanning +- โœ… **Static Type Safety** - Complete type hints with protocol-based interfaces +- โœ… **Zero Linting Issues** - Automated Ruff formatting and style enforcement +- โœ… **Property-Based Testing** - Hypothesis for robust edge case validation +- โœ… **Architecture Compliance** - AI-enforced SOLID principles and Object Calisthenics +- โœ… **Container Security** - Minimal attack surface with read-only production filesystems + +## ๐Ÿš€ Release Management + +### Smart Versioning & Naming + +- **Hybrid Calver**: `v{major}.{minor}.{YYYYMMDD}` format +- **AI-Generated Names**: Themed releases based on PR content analysis + - Performance: `"Swift Falcon"` `"Lightning Cheetah"` + - Security: `"Guardian Shield"` `"Vigilant Owl"` + - Features: `"Creative Fox"` `"Innovative Dolphin"` + - Infrastructure: `"Solid Foundation"` `"Robust Castle"` + +### Deployment Ready + +```bash +# Build production-ready container +docker build --target production -t your-project:latest . +docker run your-project:latest + +# Smart release with AI naming +@repo-manager /skill git-release +# Example: Creates v1.2.20260411 "Secure Fortress" (Docker security improvements) + +# Deploy with confidence +docker-compose -f docker-compose.prod.yml up --detach +``` + +## ๐Ÿค Contributing + +Help make this template the gold standard for Python development: + +- ๐Ÿ› **Bug Reports & Fixes** - Improve stability and reliability +- โœจ **New AI Agents & Skills** - Expand development automation +- ๐Ÿณ **Container Optimizations** - Enhance security and performance +- ๐Ÿ“š **Documentation** - Help others succeed faster +- ๐ŸŽฏ **Workflow Improvements** - Streamline development processes + +```bash +# Quick contribution workflow +git clone https://github.com/nullhack/python-project-template.git +cd python-project-template +@developer /skill feature-definition # Define your improvement +@developer /skill tdd # Test-driven implementation +@repo-manager /skill pr-management # Professional PR creation +``` + +## ๐Ÿ“„ License + +MIT License - see [`LICENSE`](LICENSE) for details. + +## ๐Ÿ™ Built With Excellence + +Standing on the shoulders of giants: + +- [OpenCode](https://opencode.ai) - Revolutionary AI-powered development platform +- [UV](https://astral.sh/uv/) - Blazing fast Python package and project manager +- [Ruff](https://astral.sh/ruff/) - Extremely fast Python linter and formatter +- [Docker](https://docker.com) - Industry-standard containerization platform +- [Distroless](https://github.com/GoogleContainerTools/distroless) - Google's minimal container images +- [Trivy](https://trivy.dev/) - Comprehensive security scanner +- [Hypothesis](https://hypothesis.readthedocs.io/) - Property-based testing framework + +--- + +**[โญ Star this repo](https://github.com/nullhack/python-project-template) to power your next breakthrough project!** + + +[contributors-shield]: https://img.shields.io/github/contributors/nullhack/python-project-template.svg?style=for-the-badge +[contributors-url]: https://github.com/nullhack/python-project-template/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/nullhack/python-project-template.svg?style=for-the-badge +[forks-url]: https://github.com/nullhack/python-project-template/network/members +[stars-shield]: https://img.shields.io/github/stars/nullhack/python-project-template.svg?style=for-the-badge +[stars-url]: https://github.com/nullhack/python-project-template/stargazers +[issues-shield]: https://img.shields.io/github/issues/nullhack/python-project-template.svg?style=for-the-badge +[issues-url]: https://github.com/nullhack/python-project-template/issues +[license-shield]: https://img.shields.io/badge/license-MIT-green?style=for-the-badge +[license-url]: https://github.com/nullhack/python-project-template/blob/main/LICENSE \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/TODO.md b/TODO.md similarity index 98% rename from {{cookiecutter.project_slug}}/TODO.md rename to TODO.md index 7280ece..ba62283 100644 --- a/{{cookiecutter.project_slug}}/TODO.md +++ b/TODO.md @@ -1,4 +1,4 @@ -# {{cookiecutter.project_name}} - Development TODO +# Python Project Template - Development TODO This file tracks current feature development within epics. For epic/feature tracking, see EPICS.md. Each session should read both TODO.md and EPICS.md to understand current state. diff --git a/cookiecutter.json b/cookiecutter.json deleted file mode 100644 index 782652d..0000000 --- a/cookiecutter.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "full_name": "eol", - "email": "nullhack@users.noreply.github.com", - "github_username": "nullhack", - "project_name": "Python Project Example", - "project_slug": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", - "package_name": "{{ cookiecutter.project_name.lower().replace(' ', '_').replace('-', '_').replace('project', 'package') }}", - "module_name": "{{ cookiecutter.package_name.lower().replace(' ', '_').replace('-', '_').replace('package', 'module') }}", - "project_short_description": "Python template with some awesome tools to quickstart any Python project", - "minimum_coverage": 100, - "include_examples": "true", - "version": "0.1.20260312", - "license": ["MIT", "BSD_3_Clause", "Apache_2.0", "GPL_3.0", "Proprietary"] -} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..963a300 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,193 @@ +# Docker Compose for python-project-template +# Production-like testing and deployment setup + +services: + # ============================================================================= + # Production application + # ============================================================================= + app: + build: + context: . + dockerfile: Dockerfile + target: production + args: + - TESTBUILD=true # Run full test suite before production build + - PYTHON_VERSION=3.13.1 + image: python-template:production + container_name: python-template-prod + environment: + - PYTHONUNBUFFERED=1 + - PYTHONDONTWRITEBYTECODE=1 + - PYTHONHASHSEED=random + # Note: No ports exposed by default for security + # Uncomment as needed: + # ports: + # - "8000:8000" + restart: unless-stopped + healthcheck: + test: ["CMD", "python", "-m", "python_package_template.python_module_template"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + # Resource limits for production + deploy: + resources: + limits: + cpus: '1.0' + memory: 512M + reservations: + cpus: '0.25' + memory: 128M + security_opt: + - no-new-privileges:true + read_only: true + tmpfs: + - /tmp:noexec,nosuid,size=100m + + # ============================================================================= + # Security scanning service + # ============================================================================= + security-scan: + build: + context: . + dockerfile: Dockerfile + target: security-scan + container_name: python-template-security + volumes: + - security-reports:/reports + command: | + sh -c " + echo '๐Ÿ” Running comprehensive security scan...' + trivy image --exit-code 0 --format json --output /reports/image-scan.json python-template:production || true + trivy fs --exit-code 0 --format json --output /reports/fs-scan.json /scan || true + echo 'โœ… Security scan completed. Reports saved to /reports/' + " + profiles: + - security + + # ============================================================================= + # Production health monitoring + # ============================================================================= + healthcheck: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-healthcheck + volumes: + - health-reports:/reports + command: | + sh -c " + echo '๐Ÿฅ Starting health monitoring...' + while true; do + echo 'Running health check...' + python -m python_package_template.python_module_template > /reports/health.log 2>&1 + echo 'Health check completed at $(date)' >> /reports/health.log + sleep 30 + done + " + profiles: + - monitoring + + # ============================================================================= + # Load testing service (simplified) + # ============================================================================= + load-test: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-load-test + volumes: + - load-reports:/reports + command: | + sh -c " + echo '๐Ÿš€ Running load tests...' + echo 'Load Test Results' > /reports/load-test.log + echo '=================' >> /reports/load-test.log + echo 'Started at: $(date)' >> /reports/load-test.log + + # Run multiple iterations + for i in {1..10}; do + echo 'Running iteration $i...' + start_time=$(date +%s.%N) + python -m python_package_template.python_module_template >> /reports/load-test.log 2>&1 + end_time=$(date +%s.%N) + duration=$(echo \"$end_time - $start_time\" | bc -l) + echo 'Iteration $i completed in ${duration}s' >> /reports/load-test.log + sleep 1 + done + + echo 'Completed at: $(date)' >> /reports/load-test.log + echo 'โœ… Load test completed' + " + profiles: + - load-test + depends_on: + - app + + # ============================================================================= + # Image analysis service + # ============================================================================= + analyze: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-analyze + volumes: + - analysis-reports:/reports + - /var/run/docker.sock:/var/run/docker.sock:ro + command: | + sh -c " + echo '๐Ÿ“Š Analyzing production image...' + + # Image size analysis + echo 'Production Image Analysis' > /reports/analysis.txt + echo '========================' >> /reports/analysis.txt + echo 'Generated at: $(date)' >> /reports/analysis.txt + echo '' >> /reports/analysis.txt + + # Note: This would require docker client in the container + # For now, we'll create a placeholder report + echo 'Image: python-template:production' >> /reports/analysis.txt + echo 'Base: distroless/python3-debian12' >> /reports/analysis.txt + echo 'Security: Non-root user, read-only filesystem' >> /reports/analysis.txt + echo 'Features: Multi-stage build, vulnerability scanning' >> /reports/analysis.txt + + echo 'โœ… Analysis completed' + " + profiles: + - analysis + +# ============================================================================= +# Named volumes for production reports and data +# ============================================================================= +volumes: + security-reports: + driver: local + name: python-template-security-reports + health-reports: + driver: local + name: python-template-health-reports + load-reports: + driver: local + name: python-template-load-reports + analysis-reports: + driver: local + name: python-template-analysis-reports + +# ============================================================================= +# Production network with isolation +# ============================================================================= +networks: + default: + name: python-template-prod-network + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..64f1265 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,185 @@ +# Docker Compose for python-project-template +# Development-focused setup with hot reload and debugging capabilities + +services: + # ============================================================================= + # Main development service + # ============================================================================= + app: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false # Skip tests in dev mode for faster startup + container_name: python-template-dev + volumes: + # Hot reload: mount source code + - ./python_package_template:/app/python_package_template:ro + - ./tests:/app/tests:ro + - ./pyproject.toml:/app/pyproject.toml:ro + # Persistent volumes for development artifacts + - dev-cache:/app/.pytest_cache + - dev-coverage:/app/htmlcov + - dev-docs:/app/docs + environment: + - PYTHONPATH=/app + - PYTHONUNBUFFERED=1 + - PYTHONDONTWRITEBYTECODE=1 + - DEVELOPMENT=true + ports: + - "8000:8000" # Main application port + - "8080:8080" # Documentation server (pdoc) + - "5678:5678" # Debug port (debugpy) + command: python -m python_package_template.python_module_template + healthcheck: + test: ["CMD", "python", "-m", "python_package_template.python_module_template"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + restart: unless-stopped + + # ============================================================================= + # Test runner service + # ============================================================================= + test: + build: + context: . + dockerfile: Dockerfile + target: test + args: + - TESTBUILD=true + container_name: python-template-test + volumes: + - ./:/app:ro + - test-reports:/app/docs/tests + - test-coverage:/app/docs/coverage + - test-mutation:/app/docs/mutation + environment: + - PYTHONPATH=/app + - PYTHONUNBUFFERED=1 + command: task test + profiles: + - test + + # ============================================================================= + # Fast test runner (development) + # ============================================================================= + test-fast: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-test-fast + volumes: + - ./:/app:ro + - test-cache:/app/.pytest_cache + environment: + - PYTHONPATH=/app + - PYTHONUNBUFFERED=1 + command: task test-fast + profiles: + - test + + # ============================================================================= + # Documentation server + # ============================================================================= + docs: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-docs + volumes: + - ./python_package_template:/app/python_package_template:ro + - ./pyproject.toml:/app/pyproject.toml:ro + - dev-docs:/app/docs + ports: + - "8080:8080" + environment: + - PYTHONPATH=/app + command: task doc-serve + profiles: + - docs + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080"] + interval: 30s + timeout: 5s + retries: 3 + + # ============================================================================= + # Code quality and linting service + # ============================================================================= + lint: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-lint + volumes: + - ./:/app:ro + environment: + - PYTHONPATH=/app + command: task lint + profiles: + - quality + + # ============================================================================= + # Static type checking service + # ============================================================================= + typecheck: + build: + context: . + dockerfile: Dockerfile + target: development + args: + - TESTBUILD=false + container_name: python-template-typecheck + volumes: + - ./:/app:ro + environment: + - PYTHONPATH=/app + command: task static-check + profiles: + - quality + +# ============================================================================= +# Named volumes for persistent development data +# ============================================================================= +volumes: + dev-cache: + driver: local + name: python-template-dev-cache + dev-coverage: + driver: local + name: python-template-dev-coverage + dev-docs: + driver: local + name: python-template-dev-docs + test-reports: + driver: local + name: python-template-test-reports + test-coverage: + driver: local + name: python-template-test-coverage + test-mutation: + driver: local + name: python-template-test-mutation + test-cache: + driver: local + name: python-template-test-cache + +# ============================================================================= +# Networks (using default bridge network) +# ============================================================================= +networks: + default: + name: python-template-network + driver: bridge \ No newline at end of file diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py deleted file mode 100644 index 0cf0c17..0000000 --- a/hooks/post_gen_project.py +++ /dev/null @@ -1,26 +0,0 @@ -{%- if cookiecutter.include_examples != "true" -%} -"""A module for removing specific directories. - -This module provides a function to remove specific directories -using the `shutil` and `pathlib` modules. - -Attributes: - REMOVE_PATHS (List[str]): A list of directory paths to be removed. - -""" - -import shutil -import os -from pathlib import Path - -REMOVE_PATHS = [ - "tests/basic_test.py" -] - -for path in REMOVE_PATHS: - p = Path(".") / Path(path) - if p and p.exists() and p.is_dir(): - shutil.rmtree(p) - elif p and p.exists() and p.is_file(): - os.remove(p) -{% endif %} diff --git a/main.py b/main.py new file mode 100644 index 0000000..ca208ee --- /dev/null +++ b/main.py @@ -0,0 +1,41 @@ +"""Test main file.""" + +import logging +from typing import Literal + +import fire + +from python_package_template.python_module_template import version + +logger = logging.getLogger(__name__) + +LOGGER_LEVELS = { + "DEBUG": logging.DEBUG, + "INFO": logging.INFO, + "WARNING": logging.WARNING, + "ERROR": logging.ERROR, + "CRITICAL": logging.CRITICAL, +} + +ValidVerbosity = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + + +def main(verbosity: ValidVerbosity = "INFO"): + """Run with --verbosity=LEVEL (DEBUG, INFO, WARNING, ERROR, CRITICAL).""" + # Validate verbosity at runtime + verbosity_upper = verbosity.upper() + if verbosity_upper not in LOGGER_LEVELS: + valid_levels = ", ".join(LOGGER_LEVELS.keys()) + raise ValueError( + f"Invalid verbosity level '{verbosity}'. Valid options: {valid_levels}" + ) + + logging.basicConfig( + level=LOGGER_LEVELS[verbosity_upper], + format="%(levelname)s - %(name)s: %(message)s", + ) + version() + + +if __name__ == "__main__": + fire.Fire(main) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b7be3e0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,201 @@ +[project] +name = "python-project-template" +version = "0.1.20260411" +description = "Python template with some awesome tools to quickstart any Python project" +readme = "README.md" +requires-python = ">=3.13" +license = { file = "LICENSE" } +authors = [ + { name = "eol", email = "nullhack@users.noreply.github.com" } +] +maintainers = [ + { name = "eol", email = "nullhack@users.noreply.github.com" } +] +dependencies = [ + "dotenv>=0.9.9", + "fire>=0.7.1", +] + +[project.urls] +Repository = "https://github.com/nullhack/python-project-template" +Documentation = "https://github.com/nullhack/python-project-template/tree/main/docs/api/" + +[project.optional-dependencies] +dev = [ + "pdoc>=14.0", + "pytest>=8.3.5", + "pytest-cov>=6.1.1", + "pytest-html>=4.1.1", + "pytest-mock>=3.14.0", + "ruff>=0.11.5", + "taskipy>=1.14.1", + "hypothesis>=6.148.4", + "mutmut>=2.5.2", + "pyright>=1.1.407", +] + +[tool.setuptools] +packages = ["python_package_template"] + +[tool.ruff.lint] +ignore = [] +select = [ + "A", + "ANN", + "ASYNC", + "B", + "C4", + "C9", + "D", + "DTZ", + "E", + "ERA", + "F", + "FURB", + "G", + "I", + "ICN", + "LOG", + "N", + "NPY", + "PD", + "PT", + "PTH", + "R", + "RUF", + "S", + "SIM", + "T20", + "TD", + "W", +] +preview = true +mccabe.max-complexity = 10 +pydocstyle.convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/**" = ["S101", "ANN", "D205", "D212", "D415", "D100", "D103"] + +[tool.pytest.ini_options] +minversion = "6.0" +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "unit", + "integration", + "system", + "acceptance", + "regression", + "smoke", + "sanity", + "performance", + "security", + "performance", +] +addopts = """ +--maxfail=10 \ +--color=yes \ +--tb=short \ +-q \ +--html=docs/tests/report.html \ +""" +testpaths = [ + "tests", + "python_package_template" +] +python_files = ["*_test.py"] +python_functions = ["test_*"] +render_collapsed = true + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:" + ] + +[tool.taskipy.tasks] +run = "python -m python_package_template.python_module_template" +test-report = """\ +pytest \ + --doctest-modules \ + --cov-config=pyproject.toml \ + --cov-report html:docs/coverage \ + --cov-report term:skip-covered \ + --cov=python_package_template \ + --cov-fail-under=100 \ + --hypothesis-show-statistics \ +""" +test = """\ +python -c "import subprocess, sys; print('Running Smoke Tests...'); sys.exit(0 if subprocess.run(['pytest', '-m', 'smoke']).returncode in (0,5) else 1)" && \ +python -c "import subprocess, sys; print('Running Unit Tests...'); sys.exit(0 if subprocess.run(['pytest', '-m', 'unit', '-m', 'not slow']).returncode in (0,5) else 1)" && \ +python -c "import subprocess, sys; print('Running Integration Tests...'); sys.exit(0 if subprocess.run(['pytest', '-m', 'integration', '-m', 'not slow']).returncode in (0,5) else 1)" && \ +python -c "print('Running Tests...');" && \ +task test-report\ +""" +test-fast = """\ +python -c "import subprocess, sys; print('Running Smoke Tests...'); sys.exit(0 if subprocess.run(['pytest', '-m', 'smoke']).returncode in (0,5) else 1)" && \ +python -c "import subprocess, sys; print('Running Unit Tests...'); sys.exit(0 if subprocess.run(['pytest', '-m', 'unit', '-m', 'not slow']).returncode in (0,5) else 1)" && \ +python -c "import subprocess, sys; print('Running Integration Tests...'); sys.exit(0 if subprocess.run(['pytest', '-m', 'integration', '-m', 'not slow']).returncode in (0,5) else 1)" \ +""" +test-slow = """\ +pytest -m slow \ +""" +ruff-check = "ruff check . --fix" +ruff-format = "ruff format ." +lint = "task ruff-check && task ruff-format" +doc-serve = "pdoc ./python_package_template --host localhost --port 8080" +doc-build = "pdoc ./python_package_template -o docs/api --search" +doc-publish = """\ +task doc-build && \ +git checkout gh-pages 2>/dev/null || git checkout -b gh-pages && \ +git rm -rf . && \ +git clean -fd && \ +cp -r docs/api/. . && \ +git add -A && \ +git commit -m "Publish API documentation" && \ +git push origin gh-pages --force && \ +git checkout main""" +mut = """ +mkdir -p docs/mutation && \ +uv run mutmut run && \ +uv run mutmut results --all true > docs/mutation/results.txt && \ +echo "Mutation Testing Summary" > docs/mutation/summary.txt && \ +echo "=======================" >> docs/mutation/summary.txt && \ +uv run mutmut results >> docs/mutation/summary.txt && \ +echo "" >> docs/mutation/summary.txt && \ +echo "Generated: $(date)" >> docs/mutation/summary.txt +""" +mut-clean = "rm -rf .mutmut-cache mutants/" +static-check = "pyright" + +[tool.mutmut] +paths_to_mutate = ["python_package_template/"] +pytest_add_cli_args_test_selection = ["tests/"] +timeout = 30.0 +also_copy = ["main.py"] +do_not_mutate = [ + "**/*_test.py", + "**/test_*.py", + "**/conftest.py", +] + +[dependency-groups] +dev = [ + "detect-secrets>=1.5.0", + "hypothesis>=6.151.12", + "mutmut>=3.5.0", + "pdoc>=16.0.0", + "pyright>=1.1.408", + "pytest>=8.3.5", + "pytest-cov>=6.1.1", + "pytest-html>=4.1.1", + "pytest-mock>=3.15.1", + "ruff>=0.11.5", + "safety>=3.7.0", + "taskipy>=1.14.1", +] diff --git a/python_package_template/__init__.py b/python_package_template/__init__.py new file mode 100644 index 0000000..6c25671 --- /dev/null +++ b/python_package_template/__init__.py @@ -0,0 +1 @@ +"""Python template with some awesome tools to quickstart any Python project.""" diff --git a/python_package_template/python_module_template.py b/python_package_template/python_module_template.py new file mode 100644 index 0000000..8e7a505 --- /dev/null +++ b/python_package_template/python_module_template.py @@ -0,0 +1,38 @@ +"""Module Docstring.""" + +import logging +import tomllib +from pathlib import Path + + +logger = logging.getLogger("python_module_template") + + +def version() -> str: + """Log version at INFO level. + + Returns: + Version string from pyproject.toml. + + Examples: + >>> result = version() # doctest: +ELLIPSIS + >>> isinstance(result, str) + True + >>> len(result) > 0 + True + >>> '.' in result # Version should contain dots + True + + """ + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + + with Path(pyproject_path).open("rb") as f: + data = tomllib.load(f) + + version_str = data["project"]["version"] + logger.info("Version: %s", version_str) + return version_str + + +if __name__ == "__main__": + version() diff --git a/scripts/check_files.py b/scripts/check_files.py deleted file mode 100755 index ea48e0e..0000000 --- a/scripts/check_files.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -"""Check that all required files exist in generated projects.""" - -import sys -from pathlib import Path - -REQUIRED_FILES = [ - "pyproject.toml", - "README.md", - "AGENTS.md", - ".opencode/agents/developer.md", - ".opencode/agents/architect.md", - ".opencode/agents/repo-manager.md", - "Dockerfile", -] - - -def check_project(project_dir: str) -> tuple[bool, list[str]]: - """Check a project for required files.""" - project_path = Path(project_dir) - missing = [] - - for file_path in REQUIRED_FILES: - full_path = project_path / file_path - if not full_path.exists(): - missing.append(file_path) - - return len(missing) == 0, missing - - -def main(): - projects = ["python-project-example", "custom-test-project"] - all_present = True - - for project in projects: - present, missing = check_project(project) - - if present: - print(f"PASS: All required files present in {project}") - else: - print(f"FAIL: Missing files in {project}:") - for f in missing: - print(f" - {f}") - all_present = False - - if all_present: - print("\nAll required files are present") - return 0 - else: - sys.exit(1) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/check_variables.py b/scripts/check_variables.py deleted file mode 100755 index 4e075d6..0000000 --- a/scripts/check_variables.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -"""Check for unsubstituted cookiecutter variables in generated projects.""" - -import sys -import re -from pathlib import Path - - -def check_project(project_dir: str) -> tuple[bool, list[str]]: - """Check a project for unsubstituted variables.""" - project_path = Path(project_dir) - errors = [] - - # Pattern to match unsubstituted cookiecutter variables - pattern = re.compile(r"\{\{cookiecutter\.[^}]+\}\}") - - for filepath in project_path.rglob("*"): - # Skip certain directories - if any( - skip in filepath.parts - for skip in ["venv", ".git", "__pycache__", ".pytest_cache"] - ): - continue - - # Only check files - if not filepath.is_file(): - continue - - # Skip binary files and certain extensions - if filepath.suffix in [".pyc", ".egg-info", ".so", ".whl"]: - continue - - try: - with open(filepath, "r", encoding="utf-8", errors="ignore") as f: - content = f.read() - - matches = pattern.findall(content) - if matches: - errors.append(f"{filepath.relative_to(project_path)}: {matches}") - except Exception: - pass - - return len(errors) == 0, errors - - -def main(): - projects = ["python-project-example", "custom-test-project"] - all_clean = True - - for project in projects: - clean, errors = check_project(project) - - if clean: - print(f"PASS: No unsubstituted variables in {project}") - else: - print(f"FAIL: Found unsubstituted variables in {project}:") - for err in errors: - print(f" {err}") - all_clean = False - - if all_clean: - print("\nNo unsubstituted cookiecutter variables found") - return 0 - else: - sys.exit(1) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/template_test.sh b/scripts/template_test.sh deleted file mode 100755 index 5e68165..0000000 --- a/scripts/template_test.sh +++ /dev/null @@ -1,281 +0,0 @@ -#!/bin/bash -# template_test.sh - Comprehensive cookiecutter template testing script - -set -e - -# Configuration -TEMPLATE_DIR=$(pwd) -TEST_DIR="/tmp/template-tests-$(date +%s)" -TIMESTAMP=$(date +%Y%m%d_%H%M%S) - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -log_info() { - echo -e "${BLUE}โ„น๏ธ $1${NC}" -} - -log_success() { - echo -e "${GREEN}โœ… $1${NC}" -} - -log_warning() { - echo -e "${YELLOW}โš ๏ธ $1${NC}" -} - -log_error() { - echo -e "${RED}โŒ $1${NC}" -} - -# Clean up function -cleanup() { - if [ -d "$TEST_DIR" ]; then - rm -rf "$TEST_DIR" - log_info "Cleaned up test directory: $TEST_DIR" - fi -} - -# Set up trap for cleanup -trap cleanup EXIT - -# Create test directory -mkdir -p "$TEST_DIR" -cd "$TEST_DIR" - -log_info "Starting cookiecutter template tests at $TIMESTAMP" -log_info "Test directory: $TEST_DIR" - -# Test 1: Default configuration -log_info "Test 1: Default configuration" -cookiecutter "$TEMPLATE_DIR" --no-input -default_project=$(ls -d */ | head -1 | sed 's/\///') -log_success "Generated project: $default_project" - -# Test 2: Custom configuration -log_info "Test 2: Custom configuration" -cookiecutter "$TEMPLATE_DIR" --no-input \ - full_name="Test Developer" \ - email="test@example.com" \ - github_username="test-dev" \ - project_name="Custom Test Project" \ - project_short_description="Testing template with custom values" \ - minimum_coverage="95" \ - license="MIT" \ - version="1.0.0" - -custom_project="custom-test-project" -log_success "Generated project: $custom_project" - -# Test 3: Edge cases -log_info "Test 3: Edge case configuration" -cookiecutter "$TEMPLATE_DIR" --no-input \ - project_name="Edge Case Project With Long Name" \ - project_short_description="This is a very long description to test edge cases and ensure the template handles it properly without breaking anything in the generated files or configuration" \ - minimum_coverage="100" \ - github_username="edge-case-user" - -edge_project="edge-case-project-with-long-name" -log_success "Generated project: $edge_project" - -# Validation function -validate_project() { - local project_dir=$1 - local project_name=$2 - - log_info "Validating $project_dir..." - - cd "$project_dir" - - # Structure validation - local required_dirs=(".opencode" ".opencode/agents" ".opencode/skills" "tests" "docs") - local required_files=("pyproject.toml" "README.md" "AGENTS.md") - - for dir in "${required_dirs[@]}"; do - if [ ! -d "$dir" ]; then - log_error "Missing directory: $dir" - return 1 - fi - done - - for file in "${required_files[@]}"; do - if [ ! -f "$file" ]; then - log_error "Missing file: $file" - return 1 - fi - done - - log_success "Project structure validation passed" - - # Check for unsubstituted variables - if grep -r "{{cookiecutter" . --exclude-dir=venv --exclude-dir=.git 2>/dev/null | grep -v ".pyc"; then - log_error "Found unsubstituted cookiecutter variables" - return 1 - fi - log_success "No unsubstituted variables found" - - # Validate pyproject.toml syntax - if ! python3 -c "import tomllib; tomllib.load(open('pyproject.toml', 'rb'))" 2>/dev/null; then - log_error "pyproject.toml syntax validation failed" - return 1 - fi - log_success "pyproject.toml syntax is valid" - - # Validate YAML frontmatter in agents - for agent in .opencode/agents/*.md; do - if [ -f "$agent" ]; then - if ! python -c " -import yaml, sys -with open('$agent', 'r') as f: - content = f.read() - if '---' in content: - parts = content.split('---') - if len(parts) >= 3: - try: - yaml.safe_load(parts[1]) - except Exception as e: - print(f'Invalid YAML in $agent: {e}') - sys.exit(1) -" 2>/dev/null; then - log_error "YAML validation failed for $agent" - return 1 - fi - fi - done - log_success "Agent YAML validation passed" - - # Validate YAML frontmatter in skills - for skill in .opencode/skills/*/SKILL.md; do - if [ -f "$skill" ]; then - if ! python -c " -import yaml, sys -with open('$skill', 'r') as f: - content = f.read() - if '---' in content: - parts = content.split('---') - if len(parts) >= 3: - try: - yaml.safe_load(parts[1]) - except Exception as e: - print(f'Invalid YAML in $skill: {e}') - sys.exit(1) -" 2>/dev/null; then - log_error "YAML validation failed for $skill" - return 1 - fi - fi - done - log_success "Skills YAML validation passed" - - # Check project name appears in key files - if ! grep -q "$project_name" README.md; then - log_error "Project name not found in README.md" - return 1 - fi - - if ! grep -q "$project_name" AGENTS.md; then - log_error "Project name not found in AGENTS.md" - return 1 - fi - log_success "Project name validation passed" - - # Install dependencies and test - log_info "Installing dependencies..." - - # Check if Python is available - if ! command -v python3 &> /dev/null; then - log_error "Python is not available" - return 1 - fi - - # Create virtual environment - python3 -m venv venv - source venv/bin/activate - - # Install UV and dependencies - if ! pip install uv &>/dev/null; then - log_error "Failed to install uv" - return 1 - fi - - if ! uv pip install '.[dev]' &>/dev/null; then - log_error "Failed to install project dependencies" - return 1 - fi - log_success "Dependencies installed" - - # Run quality checks - log_info "Running quality checks..." - - # Linting - if ! python -m ruff check . &>/dev/null; then - log_error "Ruff linting failed" - return 1 - fi - log_success "Linting passed" - - # Type checking - if ! python -m pyright . &>/dev/null; then - log_error "Type checking failed" - return 1 - fi - log_success "Type checking passed" - - # Tests - if ! python -m pytest tests/ -v &>/dev/null; then - log_error "Tests failed" - return 1 - fi - log_success "Tests passed" - - # Documentation build - if ! python -m mkdocs build &>/dev/null; then - log_error "Documentation build failed" - return 1 - fi - log_success "Documentation build passed" - - log_success "Project $project_dir validation completed successfully" - cd .. - return 0 -} - -# Run validations -log_info "Starting project validations..." - -if validate_project "$default_project" "Python Project Example"; then - log_success "Default configuration validation passed" -else - log_error "Default configuration validation failed" - exit 1 -fi - -if validate_project "$custom_project" "Custom Test Project"; then - log_success "Custom configuration validation passed" -else - log_error "Custom configuration validation failed" - exit 1 -fi - -if validate_project "$edge_project" "Edge Case Project With Long Name"; then - log_success "Edge case configuration validation passed" -else - log_error "Edge case configuration validation failed" - exit 1 -fi - -log_success "๐ŸŽ‰ All template tests passed successfully!" -echo -log_info "๐Ÿ“Š Test Summary:" -echo " - Default configuration: โœ…" -echo " - Custom configuration: โœ…" -echo " - Edge case configuration: โœ…" -echo " - Structure validation: โœ…" -echo " - YAML validation: โœ…" -echo " - Quality checks: โœ…" -echo " - Generated projects work: โœ…" -echo -log_success "Template testing completed successfully at $(date)" \ No newline at end of file diff --git a/scripts/validate_cookiecutter.py b/scripts/validate_cookiecutter.py deleted file mode 100755 index dc696bd..0000000 --- a/scripts/validate_cookiecutter.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python3 -"""Validate cookiecutter.json has required fields.""" - -import json -import sys -from pathlib import Path - - -def main(): - required = ["project_name", "package_name", "full_name", "email", "github_username"] - - with open("cookiecutter.json") as f: - data = json.load(f) - - missing = [key for key in required if key not in data] - - if missing: - print(f"FAIL: Missing required keys: {missing}") - sys.exit(1) - - print("PASS: cookiecutter.json is valid") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/validate_projects.py b/scripts/validate_projects.py deleted file mode 100755 index 63ba3f8..0000000 --- a/scripts/validate_projects.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""Validate pyproject.toml in generated projects.""" - -import sys -import tomllib -from pathlib import Path - - -def validate_project(project_dir: str) -> bool: - toml_path = Path(project_dir) / "pyproject.toml" - - if not toml_path.exists(): - print(f"FAIL: {toml_path} does not exist") - return False - - try: - with open(toml_path, "rb") as f: - data = tomllib.load(f) - - # Check required sections - if "project" not in data: - print(f"FAIL: {project_dir}/pyproject.toml missing [project] section") - return False - - required_project_keys = ["name", "version", "requires-python"] - missing = [k for k in required_project_keys if k not in data["project"]] - - if missing: - print(f"FAIL: {project_dir}/pyproject.toml missing: {missing}") - return False - - print(f"PASS: {project_dir}/pyproject.toml is valid") - return True - except Exception as e: - print(f"FAIL: {project_dir}/pyproject.toml error: {e}") - return False - - -def main(): - projects = ["python-project-example", "custom-test-project"] - all_valid = True - - for project in projects: - if not validate_project(project): - all_valid = False - - if all_valid: - print("\nAll pyproject.toml files are valid") - return 0 - else: - sys.exit(1) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/validate_yaml.py b/scripts/validate_yaml.py deleted file mode 100755 index b87c0a6..0000000 --- a/scripts/validate_yaml.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/env python3 -"""Validate YAML frontmatter in agents and skills.""" - -import sys -import yaml -from pathlib import Path - - -def validate_yaml_file(filepath: Path) -> tuple[bool, str]: - """Validate YAML frontmatter in a file.""" - if not filepath.exists(): - return False, f"File does not exist: {filepath}" - - try: - with open(filepath, "r") as f: - content = f.read() - - if "---" not in content: - return True, f"No YAML frontmatter in {filepath}" - - parts = content.split("---") - if len(parts) < 3: - return True, f"No complete YAML frontmatter in {filepath}" - - yaml_part = parts[1] - if yaml_part.strip(): - data = yaml.safe_load(yaml_part) - if data is None: - return False, f"Empty YAML frontmatter in {filepath}" - - return True, f"Valid YAML in {filepath}" - except yaml.YAMLError as e: - return False, f"YAML error in {filepath}: {e}" - except Exception as e: - return False, f"Error in {filepath}: {e}" - - -def main(): - projects = ["python-project-example", "custom-test-project"] - errors = [] - - for project in projects: - project_path = Path(project) - - # Validate agents - agents_dir = project_path / ".opencode" / "agents" - if agents_dir.exists(): - for agent_file in agents_dir.glob("*.md"): - valid, msg = validate_yaml_file(agent_file) - print(msg) - if not valid: - errors.append(msg) - - # Validate skills - skills_dir = project_path / ".opencode" / "skills" - if skills_dir.exists(): - for skill_dir in skills_dir.iterdir(): - skill_file = skill_dir / "SKILL.md" - if skill_file.exists(): - valid, msg = validate_yaml_file(skill_file) - print(msg) - if not valid: - errors.append(msg) - - if errors: - print(f"\nFAIL: Found {len(errors)} YAML errors") - for err in errors: - print(f" - {err}") - sys.exit(1) - - print("\nAll YAML frontmatter is valid") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/setup_project.py b/setup_project.py new file mode 100644 index 0000000..47b7440 --- /dev/null +++ b/setup_project.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +"""Setup a new Python project from the template. + +This script copies template files from .opencode/templates/ to the project root, +substituting templated fields with the provided parameters. + +Usage: + python setup_project.py run --github-username --project-name + --project-description [--author-name ] + [--author-email ] [--package-name ] + [--module-name ] + + python setup_project.py detect-fields +""" + +import shutil +from datetime import datetime +from pathlib import Path + +import fire + +TEMPLATES_DIR = Path(__file__).parent / ".opencode" / "templates" +ROOT_DIR = Path(__file__).parent + +ORIGINAL_PROJECT_NAME = "python-project-template" +ORIGINAL_PACKAGE_NAME = "python_package_template" +ORIGINAL_MODULE_NAME = "python_module_template" +ORIGINAL_GITHUB_USERNAME = "nullhack" +ORIGINAL_AUTHOR_NAME = "eol" +ORIGINAL_AUTHOR_EMAIL = "nullhack@users.noreply.github.com" + + +def substitute_values(content: str, replacements: dict) -> str: + """Replace all values in content based on replacements dict.""" + result = content + for original, replacement in replacements.items(): + result = result.replace(original, replacement) + return result + + +def copy_and_rename_package(src_name: str, dst_name: str) -> None: + """Copy package directory and rename references inside.""" + src_dir = ROOT_DIR / src_name + dst_dir = ROOT_DIR / dst_name + + if src_dir.exists(): + if dst_dir.exists(): + shutil.rmtree(dst_dir) + shutil.copytree(src_dir, dst_dir, dirs_exist_ok=True) + print(f"Copied package: {src_name} -> {dst_name}") + + for py_file in dst_dir.rglob("*.py"): + content = py_file.read_text(encoding="utf-8") + content = content.replace(src_name, dst_name) + py_file.write_text(content, encoding="utf-8") + print(f" Renamed in: {py_file.relative_to(ROOT_DIR)}") + + +def detect_fields() -> None: + """Show what fields would need changing.""" + today = datetime.now().strftime("%Y%m%d") + print("\nFields that would need to be changed in templates:") + print("-" * 50) + print(f" 1. GitHub Username: {ORIGINAL_GITHUB_USERNAME}") + print(f" 2. Project Name: {ORIGINAL_PROJECT_NAME}") + print(" 3. Project Description: 'Python template...'") + print(f" 4. Author Name: {ORIGINAL_AUTHOR_NAME}") + print(f" 5. Author Email: {ORIGINAL_AUTHOR_EMAIL}") + print(f" 6. Package Name: {ORIGINAL_PACKAGE_NAME}") + print(f" 7. Module Name: {ORIGINAL_MODULE_NAME}") + print(f" 8. Version: starts with 0.1.{today}") + + +def copy_directory_structure(src_dir: Path, dst_dir: Path, replacements: dict) -> None: + """Copy directory structure recursively, processing template files.""" + if dst_dir.exists(): + shutil.rmtree(dst_dir) + dst_dir.mkdir(parents=True, exist_ok=True) + + for item in src_dir.rglob("*"): + if item.is_file(): + # Calculate relative path and destination + rel_path = item.relative_to(src_dir) + dst_path = dst_dir / rel_path + dst_path.parent.mkdir(parents=True, exist_ok=True) + + # Process template files + if item.suffix == ".template": + content = item.read_text(encoding="utf-8") + content = substitute_values(content, replacements) + # Remove .template extension + dst_path = dst_path.with_suffix("") + dst_path.write_text(content, encoding="utf-8") + print(f"Created: {dst_path.relative_to(ROOT_DIR)}") + else: + # Copy non-template files as-is + shutil.copy2(item, dst_path) + print(f"Copied: {dst_path.relative_to(ROOT_DIR)}") + + +def run( + github_username: str, + project_name: str, + project_description: str, + author_name: str = "Your Name", + author_email: str = "[EMAIL]", + package_name: str = None, + module_name: str = None, +) -> None: + """Run the setup script with provided parameters.""" + if package_name is None: + package_name = project_name.replace("-", "_") + if module_name is None: + module_name = package_name + + replacements = { + ORIGINAL_GITHUB_USERNAME: github_username, + ORIGINAL_AUTHOR_NAME: author_name, + ORIGINAL_AUTHOR_EMAIL: author_email, + ORIGINAL_PROJECT_NAME: project_name, + ORIGINAL_PACKAGE_NAME: package_name, + ORIGINAL_MODULE_NAME: module_name, + } + + today = datetime.now().strftime("%Y%m%d") + replacements["0.1.20260411"] = f"0.1.{today}" + replacements[ + "Python template with some awesome tools to quickstart any Python project" + ] = project_description + + print(f"\nSetting up project: {project_name}") + print(f"Description: {project_description}") + print(f"GitHub: github.com/{github_username}/{project_name}") + print(f"Package: {package_name}") + print(f"Module: {module_name}") + print() + + # Process root-level template files + for template_file in TEMPLATES_DIR.glob("*.template"): + content = template_file.read_text(encoding="utf-8") + content = substitute_values(content, replacements) + + dst_name = template_file.stem + dst_path = ROOT_DIR / dst_name + dst_path.write_text(content, encoding="utf-8") + print(f"Created: {dst_path.relative_to(ROOT_DIR)}") + + # Process .github directory structure + github_templates_dir = TEMPLATES_DIR / ".github" + if github_templates_dir.exists(): + github_dst_dir = ROOT_DIR / ".github" + copy_directory_structure(github_templates_dir, github_dst_dir, replacements) + + # Copy and rename package directory + if package_name != ORIGINAL_PACKAGE_NAME: + copy_and_rename_package(ORIGINAL_PACKAGE_NAME, package_name) + + print("\nProject setup complete!") + print("\nNext steps:") + print(" 1. Review and update README.md with project-specific content") + print(" 2. Run: uv venv && uv pip install -e '.[dev]'") + print(" 3. Run: task test && task lint && task static-check") + print( + " 4. Initialize secrets baseline: uv run detect-secrets scan --baseline .secrets.baseline" + ) + + +if __name__ == "__main__": + fire.Fire({"run": run, "detect-fields": detect_fields}) diff --git a/{{cookiecutter.project_slug}}/tests/__init__.py b/tests/__init__.py similarity index 100% rename from {{cookiecutter.project_slug}}/tests/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a5c8f50 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,23 @@ +import pytest + + +def pytest_html_report_title(report): + report.title = "Test Report" + + +@pytest.hookimpl(tryfirst=True, hookwrapper=True) +def pytest_runtest_makereport(item, call): + outcome = yield + report = outcome.get_result() + + docstring = item.obj.__doc__ or "" + report.docstrings = docstring + + +def pytest_html_results_table_header(cells): + cells.insert(2, "Documentation") + + +def pytest_html_results_table_row(report, cells): + docstring = getattr(report, "docstrings", "") or "" + cells.insert(2, f"{docstring}") diff --git a/tests/version_test.py b/tests/version_test.py new file mode 100644 index 0000000..d220565 --- /dev/null +++ b/tests/version_test.py @@ -0,0 +1,131 @@ +"""This file contains examples of how to write tests using pytest.""" + +import logging +import tomllib +from pathlib import Path +from unittest.mock import patch + +import pytest +from hypothesis import assume, example, given +from hypothesis import strategies as st + +from python_package_template import python_module_template as m +from main import main + + +@pytest.mark.unit +def test_version_called_should_return_correct_string() -> None: + """ + Given: pyproject.toml exists with version + When: version() is called + Then: Should return version string from pyproject.toml + """ + # Read expected version from same source + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + with Path(pyproject_path).open("rb") as f: + expected = tomllib.load(f)["project"]["version"] + + result = m.version() + assert result == expected + + +@pytest.mark.unit +def test_version_called_should_log_correct_message(caplog) -> None: + """ + Given: pyproject.toml exists with version + When: version() is called + Then: Should log the exact version message format + """ + # Read expected version from same source + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + with Path(pyproject_path).open("rb") as f: + expected_version = tomllib.load(f)["project"]["version"] + + # Capture logs at INFO level + with caplog.at_level(logging.INFO): + result = m.version() + + # Verify the exact log message format + assert f"Version: {expected_version}" in caplog.text + assert result == expected_version + + +@pytest.mark.system +@example(verbosity="DEBUG") +@example(verbosity="INFO") +@given(verbosity=st.sampled_from(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])) +def test_main_with_verbosity_level_should_control_version_output( + verbosity: str, +) -> None: + """ + Given: Different verbosity levels + When: main() is called with that verbosity + Then: Version should appear in logs for DEBUG and INFO levels, but not for WARNING and above + """ + assume(verbosity != "CRITICAL") + + # Read expected version dynamically + pyproject_path = Path(__file__).parent.parent / "pyproject.toml" + with Path(pyproject_path).open("rb") as f: + expected_version = tomllib.load(f)["project"]["version"] + + expected_level = getattr(logging, verbosity.upper()) + + # Create a custom logger handler to capture messages instead of using caplog + from io import StringIO + + log_stream = StringIO() + handler = logging.StreamHandler(log_stream) + handler.setLevel(expected_level) + + # Mock logging.basicConfig to use our custom handler instead + def mock_basic_config(**kwargs): + # Set up the logger with our custom handler for testing + logger = logging.getLogger("python_module_template") + logger.handlers.clear() + logger.addHandler(handler) + logger.setLevel(kwargs.get("level", logging.INFO)) + + with patch( + "main.logging.basicConfig", side_effect=mock_basic_config + ) as mock_basicConfig: + # Call main() directly with the verbosity level + main(verbosity) + + # Verify that logging.basicConfig was called with the correct level + mock_basicConfig.assert_called_once() + args, kwargs = mock_basicConfig.call_args + assert kwargs["level"] == expected_level + + # Check the captured log output + log_output = log_stream.getvalue() + + # Standard logging behavior: DEBUG and INFO levels should show INFO messages + # WARNING, ERROR, CRITICAL levels should NOT show INFO messages + if verbosity in ["WARNING", "ERROR", "CRITICAL"]: + # These levels should NOT show INFO messages since INFO < WARNING/ERROR/CRITICAL + assert f"Version: {expected_version}" not in log_output, ( + f"Expected no version messages at {verbosity} level, but got output: {log_output!r}" + ) + else: + # DEBUG and INFO levels should show INFO messages since INFO >= DEBUG and INFO >= INFO + assert f"Version: {expected_version}" in log_output, ( + f"Expected version message at {verbosity} level, but got output: {log_output!r}" + ) + + +@pytest.mark.unit +def test_main_with_invalid_verbosity_should_raise_value_error() -> None: + """ + Given: An invalid verbosity level + When: main() is called with invalid verbosity + Then: Should raise ValueError with helpful message + """ + # Test that calling main() with invalid verbosity raises ValueError + with pytest.raises(ValueError) as exc_info: + main("INVALID_LEVEL") + + # Verify the error message contains expected details + error_message = str(exc_info.value) + assert "Invalid verbosity level 'INVALID_LEVEL'" in error_message + assert "Valid options: DEBUG, INFO, WARNING, ERROR, CRITICAL" in error_message diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..ac733cc --- /dev/null +++ b/uv.lock @@ -0,0 +1,1349 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version < '3.14'", +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + +[[package]] +name = "authlib" +version = "1.6.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.1.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872, upload-time = "2025-03-30T20:36:45.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708, upload-time = "2025-03-30T20:35:47.417Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981, upload-time = "2025-03-30T20:35:49.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495, upload-time = "2025-03-30T20:35:51.073Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538, upload-time = "2025-03-30T20:35:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561, upload-time = "2025-03-30T20:35:54.658Z" }, + { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633, upload-time = "2025-03-30T20:35:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712, upload-time = "2025-03-30T20:35:57.801Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000, upload-time = "2025-03-30T20:35:59.378Z" }, + { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195, upload-time = "2025-03-30T20:36:01.005Z" }, + { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998, upload-time = "2025-03-30T20:36:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541, upload-time = "2025-03-30T20:36:04.638Z" }, + { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767, upload-time = "2025-03-30T20:36:06.503Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997, upload-time = "2025-03-30T20:36:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708, upload-time = "2025-03-30T20:36:09.781Z" }, + { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046, upload-time = "2025-03-30T20:36:11.409Z" }, + { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139, upload-time = "2025-03-30T20:36:13.86Z" }, + { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307, upload-time = "2025-03-30T20:36:16.074Z" }, + { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116, upload-time = "2025-03-30T20:36:18.033Z" }, + { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909, upload-time = "2025-03-30T20:36:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068, upload-time = "2025-03-30T20:36:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435, upload-time = "2025-03-30T20:36:43.61Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, +] + +[[package]] +name = "detect-secrets" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/67/382a863fff94eae5a0cf05542179169a1c49a4c8784a9480621e2066ca7d/detect_secrets-1.5.0.tar.gz", hash = "sha256:6bb46dcc553c10df51475641bb30fd69d25645cc12339e46c824c1e0c388898a", size = 97351, upload-time = "2024-05-06T17:46:19.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/5e/4f5fe4b89fde1dc3ed0eb51bd4ce4c0bca406246673d370ea2ad0c58d747/detect_secrets-1.5.0-py3-none-any.whl", hash = "sha256:e24e7b9b5a35048c313e983f76c4bd09dad89f045ff059e354f9943bf45aa060", size = 120341, upload-time = "2024-05-06T17:46:16.628Z" }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + +[[package]] +name = "dparse" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/ee/96c65e17222b973f0d3d0aa9bad6a59104ca1b0eb5b659c25c2900fccd85/dparse-0.6.4.tar.gz", hash = "sha256:90b29c39e3edc36c6284c82c4132648eaf28a01863eb3c231c2512196132201a", size = 27912, upload-time = "2024-11-08T16:52:06.444Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/26/035d1c308882514a1e6ddca27f9d3e570d67a0e293e7b4d910a70c8fe32b/dparse-0.6.4-py3-none-any.whl", hash = "sha256:fbab4d50d54d0e739fbb4dedfc3d92771003a5b9aa8545ca7a7045e3b174af57", size = 11925, upload-time = "2024-11-08T16:52:03.844Z" }, +] + +[[package]] +name = "filelock" +version = "3.25.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, +] + +[[package]] +name = "fire" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "termcolor" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/00/f8d10588d2019d6d6452653def1ee807353b21983db48550318424b5ff18/fire-0.7.1.tar.gz", hash = "sha256:3b208f05c736de98fb343310d090dcc4d8c78b2a89ea4f32b837c586270a9cbf", size = 88720, upload-time = "2025-08-16T20:20:24.175Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/4c/93d0f85318da65923e4b91c1c2ff03d8a458cbefebe3bc612a6693c7906d/fire-0.7.1-py3-none-any.whl", hash = "sha256:e43fd8a5033a9001e7e2973bab96070694b9f12f2e0ecf96d4683971b5ab1882", size = 115945, upload-time = "2025-08-16T20:20:22.87Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.151.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/ab/67ca321d1ab96fd3828b12142f1c258e2d4a668a025d06cd50ab3409787f/hypothesis-6.151.12.tar.gz", hash = "sha256:be485f503979af4c3dfa19e3fc2b967d0458e7f8c4e28128d7e215e0a55102e0", size = 463900, upload-time = "2026-04-08T19:40:06.205Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/5a/6cecf134b631050a1f8605096adbe812483b60790d951470989d39b56860/hypothesis-6.151.12-py3-none-any.whl", hash = "sha256:37d4f3a768365c30571b11dfd7a6857a12173d933010b2c4ab65619f1b5952c5", size = 529656, upload-time = "2026-04-08T19:40:03.126Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "libcst" +version = "1.8.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml", marker = "python_full_version >= '3.14'" }, + { name = "pyyaml-ft", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/cd/337df968b38d94c5aabd3e1b10630f047a2b345f6e1d4456bd9fe7417537/libcst-1.8.6.tar.gz", hash = "sha256:f729c37c9317126da9475bdd06a7208eb52fcbd180a6341648b45a56b4ba708b", size = 891354, upload-time = "2025-11-03T22:33:30.621Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/01/723cd467ec267e712480c772aacc5aa73f82370c9665162fd12c41b0065b/libcst-1.8.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7445479ebe7d1aff0ee094ab5a1c7718e1ad78d33e3241e1a1ec65dcdbc22ffb", size = 2206386, upload-time = "2025-11-03T22:32:27.422Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/b944944f910f24c094f9b083f76f61e3985af5a376f5342a21e01e2d1a81/libcst-1.8.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4fc3fef8a2c983e7abf5d633e1884c5dd6fa0dcb8f6e32035abd3d3803a3a196", size = 2083945, upload-time = "2025-11-03T22:32:28.847Z" }, + { url = "https://files.pythonhosted.org/packages/36/a1/bd1b2b2b7f153d82301cdaddba787f4a9fc781816df6bdb295ca5f88b7cf/libcst-1.8.6-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1a3a5e4ee870907aa85a4076c914ae69066715a2741b821d9bf16f9579de1105", size = 2235818, upload-time = "2025-11-03T22:32:30.504Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ab/f5433988acc3b4d188c4bb154e57837df9488cc9ab551267cdeabd3bb5e7/libcst-1.8.6-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6609291c41f7ad0bac570bfca5af8fea1f4a27987d30a1fa8b67fe5e67e6c78d", size = 2301289, upload-time = "2025-11-03T22:32:31.812Z" }, + { url = "https://files.pythonhosted.org/packages/5d/57/89f4ba7a6f1ac274eec9903a9e9174890d2198266eee8c00bc27eb45ecf7/libcst-1.8.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25eaeae6567091443b5374b4c7d33a33636a2d58f5eda02135e96fc6c8807786", size = 2299230, upload-time = "2025-11-03T22:32:33.242Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/0aa693bc24cce163a942df49d36bf47a7ed614a0cd5598eee2623bc31913/libcst-1.8.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04030ea4d39d69a65873b1d4d877def1c3951a7ada1824242539e399b8763d30", size = 2408519, upload-time = "2025-11-03T22:32:34.678Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/6dd055b5f15afa640fb3304b2ee9df8b7f72e79513814dbd0a78638f4a0e/libcst-1.8.6-cp313-cp313-win_amd64.whl", hash = "sha256:8066f1b70f21a2961e96bedf48649f27dfd5ea68be5cd1bed3742b047f14acde", size = 2119853, upload-time = "2025-11-03T22:32:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/5ddb2a22f0b0abdd6dcffa40621ada1feaf252a15e5b2733a0a85dfd0429/libcst-1.8.6-cp313-cp313-win_arm64.whl", hash = "sha256:c188d06b583900e662cd791a3f962a8c96d3dfc9b36ea315be39e0a4c4792ebf", size = 1999808, upload-time = "2025-11-03T22:32:38.1Z" }, + { url = "https://files.pythonhosted.org/packages/25/d3/72b2de2c40b97e1ef4a1a1db4e5e52163fc7e7740ffef3846d30bc0096b5/libcst-1.8.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c41c76e034a1094afed7057023b1d8967f968782433f7299cd170eaa01ec033e", size = 2190553, upload-time = "2025-11-03T22:32:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/0d/20/983b7b210ccc3ad94a82db54230e92599c4a11b9cfc7ce3bc97c1d2df75c/libcst-1.8.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5432e785322aba3170352f6e72b32bea58d28abd141ac37cc9b0bf6b7c778f58", size = 2074717, upload-time = "2025-11-03T22:32:41.373Z" }, + { url = "https://files.pythonhosted.org/packages/13/f2/9e01678fedc772e09672ed99930de7355757035780d65d59266fcee212b8/libcst-1.8.6-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:85b7025795b796dea5284d290ff69de5089fc8e989b25d6f6f15b6800be7167f", size = 2225834, upload-time = "2025-11-03T22:32:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/4a/0d/7bed847b5c8c365e9f1953da274edc87577042bee5a5af21fba63276e756/libcst-1.8.6-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:536567441182a62fb706e7aa954aca034827b19746832205953b2c725d254a93", size = 2287107, upload-time = "2025-11-03T22:32:44.549Z" }, + { url = "https://files.pythonhosted.org/packages/02/f0/7e51fa84ade26c518bfbe7e2e4758b56d86a114c72d60309ac0d350426c4/libcst-1.8.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f04d3672bde1704f383a19e8f8331521abdbc1ed13abb349325a02ac56e5012", size = 2288672, upload-time = "2025-11-03T22:32:45.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cd/15762659a3f5799d36aab1bc2b7e732672722e249d7800e3c5f943b41250/libcst-1.8.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f04febcd70e1e67917be7de513c8d4749d2e09206798558d7fe632134426ea4", size = 2392661, upload-time = "2025-11-03T22:32:47.232Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6b/b7f9246c323910fcbe021241500f82e357521495dcfe419004dbb272c7cb/libcst-1.8.6-cp313-cp313t-win_amd64.whl", hash = "sha256:1dc3b897c8b0f7323412da3f4ad12b16b909150efc42238e19cbf19b561cc330", size = 2105068, upload-time = "2025-11-03T22:32:49.145Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0b/4fd40607bc4807ec2b93b054594373d7fa3d31bb983789901afcb9bcebe9/libcst-1.8.6-cp313-cp313t-win_arm64.whl", hash = "sha256:44f38139fa95e488db0f8976f9c7ca39a64d6bc09f2eceef260aa1f6da6a2e42", size = 1985181, upload-time = "2025-11-03T22:32:50.597Z" }, + { url = "https://files.pythonhosted.org/packages/3a/60/4105441989e321f7ad0fd28ffccb83eb6aac0b7cfb0366dab855dcccfbe5/libcst-1.8.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b188e626ce61de5ad1f95161b8557beb39253de4ec74fc9b1f25593324a0279c", size = 2204202, upload-time = "2025-11-03T22:32:52.311Z" }, + { url = "https://files.pythonhosted.org/packages/67/2f/51a6f285c3a183e50cfe5269d4a533c21625aac2c8de5cdf2d41f079320d/libcst-1.8.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87e74f7d7dfcba9efa91127081e22331d7c42515f0a0ac6e81d4cf2c3ed14661", size = 2083581, upload-time = "2025-11-03T22:32:54.269Z" }, + { url = "https://files.pythonhosted.org/packages/2f/64/921b1c19b638860af76cdb28bc81d430056592910b9478eea49e31a7f47a/libcst-1.8.6-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:3a926a4b42015ee24ddfc8ae940c97bd99483d286b315b3ce82f3bafd9f53474", size = 2236495, upload-time = "2025-11-03T22:32:55.723Z" }, + { url = "https://files.pythonhosted.org/packages/12/a8/b00592f9bede618cbb3df6ffe802fc65f1d1c03d48a10d353b108057d09c/libcst-1.8.6-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:3f4fbb7f569e69fd9e89d9d9caa57ca42c577c28ed05062f96a8c207594e75b8", size = 2301466, upload-time = "2025-11-03T22:32:57.337Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/790d9002f31580fefd0aec2f373a0f5da99070e04c5e8b1c995d0104f303/libcst-1.8.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:08bd63a8ce674be431260649e70fca1d43f1554f1591eac657f403ff8ef82c7a", size = 2300264, upload-time = "2025-11-03T22:32:58.852Z" }, + { url = "https://files.pythonhosted.org/packages/21/de/dc3f10e65bab461be5de57850d2910a02c24c3ddb0da28f0e6e4133c3487/libcst-1.8.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e00e275d4ba95d4963431ea3e409aa407566a74ee2bf309a402f84fc744abe47", size = 2408572, upload-time = "2025-11-03T22:33:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/20/3b/35645157a7590891038b077db170d6dd04335cd2e82a63bdaa78c3297dfe/libcst-1.8.6-cp314-cp314-win_amd64.whl", hash = "sha256:fea5c7fa26556eedf277d4f72779c5ede45ac3018650721edd77fd37ccd4a2d4", size = 2193917, upload-time = "2025-11-03T22:33:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a2/1034a9ba7d3e82f2c2afaad84ba5180f601aed676d92b76325797ad60951/libcst-1.8.6-cp314-cp314-win_arm64.whl", hash = "sha256:bb9b4077bdf8857b2483879cbbf70f1073bc255b057ec5aac8a70d901bb838e9", size = 2078748, upload-time = "2025-11-03T22:33:03.707Z" }, + { url = "https://files.pythonhosted.org/packages/95/a1/30bc61e8719f721a5562f77695e6154e9092d1bdf467aa35d0806dcd6cea/libcst-1.8.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:55ec021a296960c92e5a33b8d93e8ad4182b0eab657021f45262510a58223de1", size = 2188980, upload-time = "2025-11-03T22:33:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/2c/14/c660204532407c5628e3b615015a902ed2d0b884b77714a6bdbe73350910/libcst-1.8.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ba9ab2b012fbd53b36cafd8f4440a6b60e7e487cd8b87428e57336b7f38409a4", size = 2074828, upload-time = "2025-11-03T22:33:06.864Z" }, + { url = "https://files.pythonhosted.org/packages/82/e2/c497c354943dff644749f177ee9737b09ed811b8fc842b05709a40fe0d1b/libcst-1.8.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c0a0cc80aebd8aa15609dd4d330611cbc05e9b4216bcaeabba7189f99ef07c28", size = 2225568, upload-time = "2025-11-03T22:33:08.354Z" }, + { url = "https://files.pythonhosted.org/packages/86/ef/45999676d07bd6d0eefa28109b4f97124db114e92f9e108de42ba46a8028/libcst-1.8.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:42a4f68121e2e9c29f49c97f6154e8527cd31021809cc4a941c7270aa64f41aa", size = 2286523, upload-time = "2025-11-03T22:33:10.206Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6c/517d8bf57d9f811862f4125358caaf8cd3320a01291b3af08f7b50719db4/libcst-1.8.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a434c521fadaf9680788b50d5c21f4048fa85ed19d7d70bd40549fbaeeecab1", size = 2288044, upload-time = "2025-11-03T22:33:11.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/24d7d49478ffb61207f229239879845da40a374965874f5ee60f96b02ddb/libcst-1.8.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a65f844d813ab4ef351443badffa0ae358f98821561d19e18b3190f59e71996", size = 2392605, upload-time = "2025-11-03T22:33:12.962Z" }, + { url = "https://files.pythonhosted.org/packages/39/c3/829092ead738b71e96a4e96896c96f276976e5a8a58b4473ed813d7c962b/libcst-1.8.6-cp314-cp314t-win_amd64.whl", hash = "sha256:bdb14bc4d4d83a57062fed2c5da93ecb426ff65b0dc02ddf3481040f5f074a82", size = 2181581, upload-time = "2025-11-03T22:33:14.514Z" }, + { url = "https://files.pythonhosted.org/packages/98/6d/5d6a790a02eb0d9d36c4aed4f41b277497e6178900b2fa29c35353aa45ed/libcst-1.8.6-cp314-cp314t-win_arm64.whl", hash = "sha256:819c8081e2948635cab60c603e1bbdceccdfe19104a242530ad38a36222cb88f", size = 2065000, upload-time = "2025-11-03T22:33:16.257Z" }, +] + +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + +[[package]] +name = "markdown2" +version = "2.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/ae/07d4a5fcaa5509221287d289323d75ac8eda5a5a4ac9de2accf7bbcc2b88/markdown2-2.5.5.tar.gz", hash = "sha256:001547e68f6e7fcf0f1cb83f7e82f48aa7d48b2c6a321f0cd20a853a8a2d1664", size = 157249, upload-time = "2026-03-02T20:46:53.411Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/af/4b3891eb0a49d6cfd5cbf3e9bf514c943afc2b0f13e2c57cc57cd88ecc21/markdown2-2.5.5-py3-none-any.whl", hash = "sha256:be798587e09d1f52d2e4d96a649c4b82a778c75f9929aad52a2c95747fa26941", size = 56250, upload-time = "2026-03-02T20:46:52.032Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "marshmallow" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/7e/1dbd4096eb7c148cd2841841916f78820bb85a4d80a0c25c02d30815a7fb/marshmallow-4.3.0.tar.gz", hash = "sha256:fb43c53b3fe240b8f6af37223d6ef1636f927ad9bea8ab323afad95dff090880", size = 224485, upload-time = "2026-04-03T21:46:32.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/e0/ff24e25218bb59eb6290a530cea40651b14068b6e3659b20f9c175179632/marshmallow-4.3.0-py3-none-any.whl", hash = "sha256:46c4fe6984707e3cbd485dfebbf0a59874f58d695aad05c1668d15e8c6e13b46", size = 49148, upload-time = "2026-04-03T21:46:31.241Z" }, +] + +[[package]] +name = "mdit-py-plugins" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mslex" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583, upload-time = "2024-10-16T13:16:18.523Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820, upload-time = "2024-10-16T13:16:17.566Z" }, +] + +[[package]] +name = "mutmut" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "coverage" }, + { name = "libcst" }, + { name = "pytest" }, + { name = "setproctitle" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/0d/9ce4fc8b219504a336eb814c5a7ea8e379ad93ce05327ff3842aea93bf0b/mutmut-3.5.0.tar.gz", hash = "sha256:548186d4b0c494b7b9895db82871cb1f229b9271c9ff7cd633e348dd9afcc772", size = 36389, upload-time = "2026-02-22T18:46:41.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/23/ac475f6db39643946feb09290a2178d603d2b623034d56d3f5059cddb769/mutmut-3.5.0-py3-none-any.whl", hash = "sha256:f19f2dd2e977eb9dc17255d8cb11e24fbfc3191620fba3108cac25779c9d78c9", size = 34242, upload-time = "2026-02-22T18:46:43.113Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950, upload-time = "2024-11-08T09:47:47.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451, upload-time = "2024-11-08T09:47:44.722Z" }, +] + +[[package]] +name = "pdoc" +version = "16.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown2" }, + { name = "markupsafe" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/fe/ab3f34a5fb08c6b698439a2c2643caf8fef0d61a86dd3fdcd5501c670ab8/pdoc-16.0.0.tar.gz", hash = "sha256:fdadc40cc717ec53919e3cd720390d4e3bcd40405cb51c4918c119447f913514", size = 111890, upload-time = "2025-10-27T16:02:16.345Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/a1/56a17b7f9e18c2bb8df73f3833345d97083b344708b97bab148fdd7e0b82/pdoc-16.0.0-py3-none-any.whl", hash = "sha256:070b51de2743b9b1a4e0ab193a06c9e6c12cf4151cf9137656eebb16e8556628", size = 100014, upload-time = "2025-10-27T16:02:15.007Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "psutil" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502, upload-time = "2024-12-19T18:21:20.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511, upload-time = "2024-12-19T18:21:45.163Z" }, + { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985, upload-time = "2024-12-19T18:21:49.254Z" }, + { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488, upload-time = "2024-12-19T18:21:51.638Z" }, + { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477, upload-time = "2024-12-19T18:21:55.306Z" }, + { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017, upload-time = "2024-12-19T18:21:57.875Z" }, + { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602, upload-time = "2024-12-19T18:22:08.808Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444, upload-time = "2024-12-19T18:22:11.335Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857, upload-time = "2025-04-05T14:07:51.592Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841, upload-time = "2025-04-05T14:07:49.641Z" }, +] + +[[package]] +name = "pytest-html" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pytest" }, + { name = "pytest-metadata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773, upload-time = "2023-11-07T15:44:28.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491, upload-time = "2023-11-07T15:44:27.149Z" }, +] + +[[package]] +name = "pytest-metadata" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952, upload-time = "2024-02-12T19:38:44.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428, upload-time = "2024-02-12T19:38:42.531Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-project-template" +version = "0.1.20260411" +source = { virtual = "." } +dependencies = [ + { name = "dotenv" }, + { name = "fire" }, +] + +[package.optional-dependencies] +dev = [ + { name = "hypothesis" }, + { name = "mutmut" }, + { name = "pdoc" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-html" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "taskipy" }, +] + +[package.dev-dependencies] +dev = [ + { name = "detect-secrets" }, + { name = "hypothesis" }, + { name = "mutmut" }, + { name = "pdoc" }, + { name = "pyright" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-html" }, + { name = "pytest-mock" }, + { name = "ruff" }, + { name = "safety" }, + { name = "taskipy" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "fire", specifier = ">=0.7.1" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.148.4" }, + { name = "mutmut", marker = "extra == 'dev'", specifier = ">=2.5.2" }, + { name = "pdoc", marker = "extra == 'dev'", specifier = ">=14.0" }, + { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.407" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, + { name = "pytest-html", marker = "extra == 'dev'", specifier = ">=4.1.1" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.5" }, + { name = "taskipy", marker = "extra == 'dev'", specifier = ">=1.14.1" }, +] +provides-extras = ["dev"] + +[package.metadata.requires-dev] +dev = [ + { name = "detect-secrets", specifier = ">=1.5.0" }, + { name = "hypothesis", specifier = ">=6.151.12" }, + { name = "mutmut", specifier = ">=3.5.0" }, + { name = "pdoc", specifier = ">=16.0.0" }, + { name = "pyright", specifier = ">=1.1.408" }, + { name = "pytest", specifier = ">=8.3.5" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "pytest-html", specifier = ">=4.1.1" }, + { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.11.5" }, + { name = "safety", specifier = ">=3.7.0" }, + { name = "taskipy", specifier = ">=1.14.1" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-ft" +version = "8.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/eb/5a0d575de784f9a1f94e2b1288c6886f13f34185e13117ed530f32b6f8a8/pyyaml_ft-8.0.0.tar.gz", hash = "sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab", size = 141057, upload-time = "2025-06-10T15:32:15.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/ba/a067369fe61a2e57fb38732562927d5bae088c73cb9bb5438736a9555b29/pyyaml_ft-8.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6", size = 187027, upload-time = "2025-06-10T15:31:48.722Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c5/a3d2020ce5ccfc6aede0d45bcb870298652ac0cf199f67714d250e0cdf39/pyyaml_ft-8.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69", size = 176146, upload-time = "2025-06-10T15:31:50.584Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bb/23a9739291086ca0d3189eac7cd92b4d00e9fdc77d722ab610c35f9a82ba/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0", size = 746792, upload-time = "2025-06-10T15:31:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/5f/c2/e8825f4ff725b7e560d62a3609e31d735318068e1079539ebfde397ea03e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42", size = 786772, upload-time = "2025-06-10T15:31:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/35/be/58a4dcae8854f2fdca9b28d9495298fd5571a50d8430b1c3033ec95d2d0e/pyyaml_ft-8.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b", size = 778723, upload-time = "2025-06-10T15:31:56.093Z" }, + { url = "https://files.pythonhosted.org/packages/86/ed/fed0da92b5d5d7340a082e3802d84c6dc9d5fa142954404c41a544c1cb92/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254", size = 758478, upload-time = "2025-06-10T15:31:58.314Z" }, + { url = "https://files.pythonhosted.org/packages/f0/69/ac02afe286275980ecb2dcdc0156617389b7e0c0a3fcdedf155c67be2b80/pyyaml_ft-8.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8", size = 799159, upload-time = "2025-06-10T15:31:59.675Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ac/c492a9da2e39abdff4c3094ec54acac9747743f36428281fb186a03fab76/pyyaml_ft-8.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96", size = 158779, upload-time = "2025-06-10T15:32:01.029Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/41998df3298960d7c67653669f37710fa2d568a5fc933ea24a6df60acaf6/pyyaml_ft-8.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb", size = 191331, upload-time = "2025-06-10T15:32:02.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/16/2710c252ee04cbd74d9562ebba709e5a284faeb8ada88fcda548c9191b47/pyyaml_ft-8.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1", size = 182879, upload-time = "2025-06-10T15:32:04.466Z" }, + { url = "https://files.pythonhosted.org/packages/9a/40/ae8163519d937fa7bfa457b6f78439cc6831a7c2b170e4f612f7eda71815/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49", size = 811277, upload-time = "2025-06-10T15:32:06.214Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/28d82dbff7f87b96f0eeac79b7d972a96b4980c1e445eb6a857ba91eda00/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b", size = 831650, upload-time = "2025-06-10T15:32:08.076Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/161c4566facac7d75a9e182295c223060373d4116dead9cc53a265de60b9/pyyaml_ft-8.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a", size = 815755, upload-time = "2025-06-10T15:32:09.435Z" }, + { url = "https://files.pythonhosted.org/packages/05/10/f42c48fa5153204f42eaa945e8d1fd7c10d6296841dcb2447bf7da1be5c4/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e", size = 810403, upload-time = "2025-06-10T15:32:11.051Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d2/e369064aa51009eb9245399fd8ad2c562bd0bcd392a00be44b2a824ded7c/pyyaml_ft-8.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255", size = 835581, upload-time = "2025-06-10T15:32:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/c0/28/26534bed77109632a956977f60d8519049f545abc39215d086e33a61f1f2/pyyaml_ft-8.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793", size = 171579, upload-time = "2025-06-10T15:32:14.34Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "rich" +version = "14.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/67/cae617f1351490c25a4b8ac3b8b63a4dda609295d8222bad12242dfdc629/rich-14.3.4.tar.gz", hash = "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9", size = 230524, upload-time = "2026-04-11T02:57:45.419Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/76/6d163cfac87b632216f71879e6b2cf17163f773ff59c00b5ff4900a80fa3/rich-14.3.4-py3-none-any.whl", hash = "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", size = 310480, upload-time = "2026-04-11T02:57:47.484Z" }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488, upload-time = "2025-04-10T17:13:29.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150, upload-time = "2025-04-10T17:12:37.886Z" }, + { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637, upload-time = "2025-04-10T17:12:41.602Z" }, + { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012, upload-time = "2025-04-10T17:12:44.584Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338, upload-time = "2025-04-10T17:12:47.172Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277, upload-time = "2025-04-10T17:12:50.628Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614, upload-time = "2025-04-10T17:12:53.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873, upload-time = "2025-04-10T17:12:56.956Z" }, + { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190, upload-time = "2025-04-10T17:13:00.194Z" }, + { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301, upload-time = "2025-04-10T17:13:03.246Z" }, + { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132, upload-time = "2025-04-10T17:13:06.209Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937, upload-time = "2025-04-10T17:13:08.855Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683, upload-time = "2025-04-10T17:13:11.378Z" }, + { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217, upload-time = "2025-04-10T17:13:14.565Z" }, + { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521, upload-time = "2025-04-10T17:13:17.8Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697, upload-time = "2025-04-10T17:13:20.582Z" }, + { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665, upload-time = "2025-04-10T17:13:23.349Z" }, + { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287, upload-time = "2025-04-10T17:13:26.538Z" }, +] + +[[package]] +name = "safety" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "authlib" }, + { name = "click" }, + { name = "dparse" }, + { name = "filelock" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "marshmallow" }, + { name = "nltk" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "ruamel-yaml" }, + { name = "safety-schemas" }, + { name = "tenacity" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/e8/1cfffa0d8836de8aa31f4fa7fdeb892c7cfa97cd555039ad5df71ce0e968/safety-3.7.0.tar.gz", hash = "sha256:daec15a393cafc32b846b7ef93f9c952a1708863e242341ab5bde2e4beabb54e", size = 330538, upload-time = "2025-11-06T20:10:15.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/55/c4b2058ca346e58124ba082a3596e30dc1f5793710f8173156c7c2d77048/safety-3.7.0-py3-none-any.whl", hash = "sha256:65e71db45eb832e8840e3456333d44c23927423753d5610596a09e909a66d2bf", size = 312436, upload-time = "2025-11-06T20:10:13.576Z" }, +] + +[[package]] +name = "safety-schemas" +version = "0.0.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dparse" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "ruamel-yaml" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/ef/0e07dfdb4104c4e42ae9fc6e8a0da7be2d72ac2ee198b32f7500796de8f3/safety_schemas-0.0.16.tar.gz", hash = "sha256:3bb04d11bd4b5cc79f9fa183c658a6a8cf827a9ceec443a5ffa6eed38a50a24e", size = 54815, upload-time = "2025-09-16T14:35:31.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/a2/7840cc32890ce4b84668d3d9dfe15a48355b683ae3fb627ac97ac5a4265f/safety_schemas-0.0.16-py3-none-any.whl", hash = "sha256:6760515d3fd1e6535b251cd73014bd431d12fe0bfb8b6e8880a9379b5ab7aa44", size = 39292, upload-time = "2025-09-16T14:35:32.84Z" }, +] + +[[package]] +name = "setproctitle" +version = "1.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/48/49393a96a2eef1ab418b17475fb92b8fcfad83d099e678751b05472e69de/setproctitle-1.3.7.tar.gz", hash = "sha256:bc2bc917691c1537d5b9bca1468437176809c7e11e5694ca79a9ca12345dcb9e", size = 27002, upload-time = "2025-09-05T12:51:25.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/2f/fcedcade3b307a391b6e17c774c6261a7166aed641aee00ed2aad96c63ce/setproctitle-1.3.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3736b2a423146b5e62230502e47e08e68282ff3b69bcfe08a322bee73407922", size = 18047, upload-time = "2025-09-05T12:49:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/23/ae/afc141ca9631350d0a80b8f287aac79a76f26b6af28fd8bf92dae70dc2c5/setproctitle-1.3.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3384e682b158d569e85a51cfbde2afd1ab57ecf93ea6651fe198d0ba451196ee", size = 13073, upload-time = "2025-09-05T12:49:51.46Z" }, + { url = "https://files.pythonhosted.org/packages/87/ed/0a4f00315bc02510395b95eec3d4aa77c07192ee79f0baae77ea7b9603d8/setproctitle-1.3.7-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0564a936ea687cd24dffcea35903e2a20962aa6ac20e61dd3a207652401492dd", size = 33284, upload-time = "2025-09-05T12:49:52.741Z" }, + { url = "https://files.pythonhosted.org/packages/fc/e4/adf3c4c0a2173cb7920dc9df710bcc67e9bcdbf377e243b7a962dc31a51a/setproctitle-1.3.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5d1cb3f81531f0eb40e13246b679a1bdb58762b170303463cb06ecc296f26d0", size = 34104, upload-time = "2025-09-05T12:49:54.416Z" }, + { url = "https://files.pythonhosted.org/packages/52/4f/6daf66394152756664257180439d37047aa9a1cfaa5e4f5ed35e93d1dc06/setproctitle-1.3.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a7d159e7345f343b44330cbba9194169b8590cb13dae940da47aa36a72aa9929", size = 35982, upload-time = "2025-09-05T12:49:56.295Z" }, + { url = "https://files.pythonhosted.org/packages/1b/62/f2c0595403cf915db031f346b0e3b2c0096050e90e0be658a64f44f4278a/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0b5074649797fd07c72ca1f6bff0406f4a42e1194faac03ecaab765ce605866f", size = 33150, upload-time = "2025-09-05T12:49:58.025Z" }, + { url = "https://files.pythonhosted.org/packages/a0/29/10dd41cde849fb2f9b626c846b7ea30c99c81a18a5037a45cc4ba33c19a7/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:61e96febced3f61b766115381d97a21a6265a0f29188a791f6df7ed777aef698", size = 34463, upload-time = "2025-09-05T12:49:59.424Z" }, + { url = "https://files.pythonhosted.org/packages/71/3c/cedd8eccfaf15fb73a2c20525b68c9477518917c9437737fa0fda91e378f/setproctitle-1.3.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:047138279f9463f06b858e579cc79580fbf7a04554d24e6bddf8fe5dddbe3d4c", size = 32848, upload-time = "2025-09-05T12:50:01.107Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3e/0a0e27d1c9926fecccfd1f91796c244416c70bf6bca448d988638faea81d/setproctitle-1.3.7-cp313-cp313-win32.whl", hash = "sha256:7f47accafac7fe6535ba8ba9efd59df9d84a6214565108d0ebb1199119c9cbbd", size = 12544, upload-time = "2025-09-05T12:50:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/36/1b/6bf4cb7acbbd5c846ede1c3f4d6b4ee52744d402e43546826da065ff2ab7/setproctitle-1.3.7-cp313-cp313-win_amd64.whl", hash = "sha256:fe5ca35aeec6dc50cabab9bf2d12fbc9067eede7ff4fe92b8f5b99d92e21263f", size = 13235, upload-time = "2025-09-05T12:50:16.89Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a4/d588d3497d4714750e3eaf269e9e8985449203d82b16b933c39bd3fc52a1/setproctitle-1.3.7-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:10e92915c4b3086b1586933a36faf4f92f903c5554f3c34102d18c7d3f5378e9", size = 18058, upload-time = "2025-09-05T12:50:02.501Z" }, + { url = "https://files.pythonhosted.org/packages/05/77/7637f7682322a7244e07c373881c7e982567e2cb1dd2f31bd31481e45500/setproctitle-1.3.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:de879e9c2eab637f34b1a14c4da1e030c12658cdc69ee1b3e5be81b380163ce5", size = 13072, upload-time = "2025-09-05T12:50:03.601Z" }, + { url = "https://files.pythonhosted.org/packages/52/09/f366eca0973cfbac1470068d1313fa3fe3de4a594683385204ec7f1c4101/setproctitle-1.3.7-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c18246d88e227a5b16248687514f95642505000442165f4b7db354d39d0e4c29", size = 34490, upload-time = "2025-09-05T12:50:04.948Z" }, + { url = "https://files.pythonhosted.org/packages/71/36/611fc2ed149fdea17c3677e1d0df30d8186eef9562acc248682b91312706/setproctitle-1.3.7-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7081f193dab22df2c36f9fc6d113f3793f83c27891af8fe30c64d89d9a37e152", size = 35267, upload-time = "2025-09-05T12:50:06.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/a4/64e77d0671446bd5a5554387b69e1efd915274686844bea733714c828813/setproctitle-1.3.7-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cc9b901ce129350637426a89cfd650066a4adc6899e47822e2478a74023ff7c", size = 37376, upload-time = "2025-09-05T12:50:07.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/bc/ad9c664fe524fb4a4b2d3663661a5c63453ce851736171e454fa2cdec35c/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:80e177eff2d1ec172188d0d7fd9694f8e43d3aab76a6f5f929bee7bf7894e98b", size = 33963, upload-time = "2025-09-05T12:50:09.056Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a36de7caf2d90c4c28678da1466b47495cbbad43badb4e982d8db8167ed4/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:23e520776c445478a67ee71b2a3c1ffdafbe1f9f677239e03d7e2cc635954e18", size = 35550, upload-time = "2025-09-05T12:50:10.791Z" }, + { url = "https://files.pythonhosted.org/packages/dd/68/17e8aea0ed5ebc17fbf03ed2562bfab277c280e3625850c38d92a7b5fcd9/setproctitle-1.3.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5fa1953126a3b9bd47049d58c51b9dac72e78ed120459bd3aceb1bacee72357c", size = 33727, upload-time = "2025-09-05T12:50:12.032Z" }, + { url = "https://files.pythonhosted.org/packages/b2/33/90a3bf43fe3a2242b4618aa799c672270250b5780667898f30663fd94993/setproctitle-1.3.7-cp313-cp313t-win32.whl", hash = "sha256:4a5e212bf438a4dbeece763f4962ad472c6008ff6702e230b4f16a037e2f6f29", size = 12549, upload-time = "2025-09-05T12:50:13.074Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/50d1f07f3032e1f23d814ad6462bc0a138f369967c72494286b8a5228e40/setproctitle-1.3.7-cp313-cp313t-win_amd64.whl", hash = "sha256:cf2727b733e90b4f874bac53e3092aa0413fe1ea6d4f153f01207e6ce65034d9", size = 13243, upload-time = "2025-09-05T12:50:14.146Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/43ac3a98414f91d1b86a276bc2f799ad0b4b010e08497a95750d5bc42803/setproctitle-1.3.7-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:80c36c6a87ff72eabf621d0c79b66f3bdd0ecc79e873c1e9f0651ee8bf215c63", size = 18052, upload-time = "2025-09-05T12:50:17.928Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2c/dc258600a25e1a1f04948073826bebc55e18dbd99dc65a576277a82146fa/setproctitle-1.3.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b53602371a52b91c80aaf578b5ada29d311d12b8a69c0c17fbc35b76a1fd4f2e", size = 13071, upload-time = "2025-09-05T12:50:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/26/8e3bb082992f19823d831f3d62a89409deb6092e72fc6940962983ffc94f/setproctitle-1.3.7-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fcb966a6c57cf07cc9448321a08f3be6b11b7635be502669bc1d8745115d7e7f", size = 33180, upload-time = "2025-09-05T12:50:20.395Z" }, + { url = "https://files.pythonhosted.org/packages/f1/af/ae692a20276d1159dd0cf77b0bcf92cbb954b965655eb4a69672099bb214/setproctitle-1.3.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46178672599b940368d769474fe13ecef1b587d58bb438ea72b9987f74c56ea5", size = 34043, upload-time = "2025-09-05T12:50:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/34/b2/6a092076324dd4dac1a6d38482bedebbff5cf34ef29f58585ec76e47bc9d/setproctitle-1.3.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7f9e9e3ff135cbcc3edd2f4cf29b139f4aca040d931573102742db70ff428c17", size = 35892, upload-time = "2025-09-05T12:50:23.937Z" }, + { url = "https://files.pythonhosted.org/packages/1c/1a/8836b9f28cee32859ac36c3df85aa03e1ff4598d23ea17ca2e96b5845a8f/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:14c7eba8d90c93b0e79c01f0bd92a37b61983c27d6d7d5a3b5defd599113d60e", size = 32898, upload-time = "2025-09-05T12:50:25.617Z" }, + { url = "https://files.pythonhosted.org/packages/ef/22/8fabdc24baf42defb599714799d8445fe3ae987ec425a26ec8e80ea38f8e/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9e64e98077fb30b6cf98073d6c439cd91deb8ebbf8fc62d9dbf52bd38b0c6ac0", size = 34308, upload-time = "2025-09-05T12:50:26.827Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/b9bee9de6c8cdcb3b3a6cb0b3e773afdb86bbbc1665a3bfa424a4294fda2/setproctitle-1.3.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b91387cc0f02a00ac95dcd93f066242d3cca10ff9e6153de7ee07069c6f0f7c8", size = 32536, upload-time = "2025-09-05T12:50:28.5Z" }, + { url = "https://files.pythonhosted.org/packages/37/0c/75e5f2685a5e3eda0b39a8b158d6d8895d6daf3ba86dec9e3ba021510272/setproctitle-1.3.7-cp314-cp314-win32.whl", hash = "sha256:52b054a61c99d1b72fba58b7f5486e04b20fefc6961cd76722b424c187f362ed", size = 12731, upload-time = "2025-09-05T12:50:43.955Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ae/acddbce90d1361e1786e1fb421bc25baeb0c22ef244ee5d0176511769ec8/setproctitle-1.3.7-cp314-cp314-win_amd64.whl", hash = "sha256:5818e4080ac04da1851b3ec71e8a0f64e3748bf9849045180566d8b736702416", size = 13464, upload-time = "2025-09-05T12:50:45.057Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/20886c8ff2e6d85e3cabadab6aab9bb90acaf1a5cfcb04d633f8d61b2626/setproctitle-1.3.7-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6fc87caf9e323ac426910306c3e5d3205cd9f8dcac06d233fcafe9337f0928a3", size = 18062, upload-time = "2025-09-05T12:50:29.78Z" }, + { url = "https://files.pythonhosted.org/packages/9a/60/26dfc5f198715f1343b95c2f7a1c16ae9ffa45bd89ffd45a60ed258d24ea/setproctitle-1.3.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6134c63853d87a4897ba7d5cc0e16abfa687f6c66fc09f262bb70d67718f2309", size = 13075, upload-time = "2025-09-05T12:50:31.604Z" }, + { url = "https://files.pythonhosted.org/packages/21/9c/980b01f50d51345dd513047e3ba9e96468134b9181319093e61db1c47188/setproctitle-1.3.7-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1403d2abfd32790b6369916e2313dffbe87d6b11dca5bbd898981bcde48e7a2b", size = 34744, upload-time = "2025-09-05T12:50:32.777Z" }, + { url = "https://files.pythonhosted.org/packages/86/b4/82cd0c86e6d1c4538e1a7eb908c7517721513b801dff4ba3f98ef816a240/setproctitle-1.3.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7c5bfe4228ea22373e3025965d1a4116097e555ee3436044f5c954a5e63ac45", size = 35589, upload-time = "2025-09-05T12:50:34.13Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4f/9f6b2a7417fd45673037554021c888b31247f7594ff4bd2239918c5cd6d0/setproctitle-1.3.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:585edf25e54e21a94ccb0fe81ad32b9196b69ebc4fc25f81da81fb8a50cca9e4", size = 37698, upload-time = "2025-09-05T12:50:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/20/92/927b7d4744aac214d149c892cb5fa6dc6f49cfa040cb2b0a844acd63dcaf/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:96c38cdeef9036eb2724c2210e8d0b93224e709af68c435d46a4733a3675fee1", size = 34201, upload-time = "2025-09-05T12:50:36.697Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0c/fd4901db5ba4b9d9013e62f61d9c18d52290497f956745cd3e91b0d80f90/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:45e3ef48350abb49cf937d0a8ba15e42cee1e5ae13ca41a77c66d1abc27a5070", size = 35801, upload-time = "2025-09-05T12:50:38.314Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/54b496ac724e60e61cc3447f02690105901ca6d90da0377dffe49ff99fc7/setproctitle-1.3.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1fae595d032b30dab4d659bece20debd202229fce12b55abab978b7f30783d73", size = 33958, upload-time = "2025-09-05T12:50:39.841Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a8/c84bb045ebf8c6fdc7f7532319e86f8380d14bbd3084e6348df56bdfe6fd/setproctitle-1.3.7-cp314-cp314t-win32.whl", hash = "sha256:02432f26f5d1329ab22279ff863c83589894977063f59e6c4b4845804a08f8c2", size = 12745, upload-time = "2025-09-05T12:50:41.377Z" }, + { url = "https://files.pythonhosted.org/packages/08/b6/3a5a4f9952972791a9114ac01dfc123f0df79903577a3e0a7a404a695586/setproctitle-1.3.7-cp314-cp314t-win_amd64.whl", hash = "sha256:cbc388e3d86da1f766d8fc2e12682e446064c01cea9f88a88647cfe7c011de6a", size = 13469, upload-time = "2025-09-05T12:50:42.67Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "taskipy" +version = "1.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "mslex", marker = "sys_platform == 'win32'" }, + { name = "psutil" }, + { name = "tomli", marker = "python_full_version < '4'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/44/572261df3db9c6c3332f8618fafeb07a578fd18b06673c73f000f3586749/taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed", size = 14475, upload-time = "2024-11-26T16:37:46.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/97/4e4cfb1391c81e926bebe3d68d5231b5dbc3bb41c6ba48349e68a881462d/taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1", size = 13052, upload-time = "2024-11-26T16:37:44.546Z" }, +] + +[[package]] +name = "tenacity" +version = "9.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/c6/ee486fd809e357697ee8a44d3d69222b344920433d3b6666ccd9b374630c/tenacity-9.1.4.tar.gz", hash = "sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a", size = 49413, upload-time = "2026-02-07T10:45:33.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c1/eb8f9debc45d3b7918a32ab756658a0904732f75e555402972246b0b8e71/tenacity-9.1.4-py3-none-any.whl", hash = "sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55", size = 28926, upload-time = "2026-02-07T10:45:32.24Z" }, +] + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "textual" +version = "8.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/2f/d44f0f12b3ddb1f0b88f7775652e99c6b5a43fd733badf4ce064bdbfef4a/textual-8.2.3.tar.gz", hash = "sha256:beea7b86b03b03558a2224f0cc35252e60ef8b0c4353b117b2f40972902d976a", size = 1848738, upload-time = "2026-04-05T09:12:45.338Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/28/a81d6ce9f4804818bd1231a9a6e4d56ea84ebbe8385c49591444f0234fa2/textual-8.2.3-py3-none-any.whl", hash = "sha256:5008ac581bebf1f6fa0520404261844a231e5715fdbddd10ca73916a3af48ca2", size = 724231, upload-time = "2026-04-05T09:12:48.747Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/af/14b24e41977adb296d6bd1fb59402cf7d60ce364f90c890bd2ec65c43b5a/tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064", size = 187167, upload-time = "2026-01-13T01:14:53.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/11/87d6d29fb5d237229d67973a6c9e06e048f01cf4994dee194ab0ea841814/tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680", size = 39310, upload-time = "2026-01-13T01:14:51.965Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "typer" +version = "0.23.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/07/b822e1b307d40e263e8253d2384cf98c51aa2368cc7ba9a07e523a1d964b/typer-0.23.1.tar.gz", hash = "sha256:2070374e4d31c83e7b61362fd859aa683576432fd5b026b060ad6b4cd3b86134", size = 120047, upload-time = "2026-02-13T10:04:30.984Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/91/9b286ab899c008c2cb05e8be99814807e7fbbd33f0c0c960470826e5ac82/typer-0.23.1-py3-none-any.whl", hash = "sha256:3291ad0d3c701cbf522012faccfbb29352ff16ad262db2139e6b01f15781f14e", size = 56813, upload-time = "2026-02-13T10:04:32.008Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] diff --git a/validate-docker.py b/validate-docker.py new file mode 100644 index 0000000..6c21263 --- /dev/null +++ b/validate-docker.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +Docker Setup Validation Script +Validates the new Docker configuration without requiring Docker to be installed. +""" + +import re +from pathlib import Path + + +def validate_dockerfile(dockerfile_path: Path) -> list[str]: + """Validate Dockerfile syntax and best practices.""" + issues = [] + + if not dockerfile_path.exists(): + issues.append(f"Dockerfile not found: {dockerfile_path}") + return issues + + content = dockerfile_path.read_text() + lines = content.split("\n") + + # Check for syntax directive + if not content.startswith("# syntax="): + issues.append("Missing BuildKit syntax directive") + + # Check for multi-stage build + from_count = len(re.findall(r"^FROM .* AS ", content, re.MULTILINE)) + if from_count < 2: + issues.append( + "Should use multi-stage build (found {} stages)".format(from_count) + ) + + # Check for security practices + if "USER root" in content: + issues.append("Avoid running as root user") + + if "--mount=type=cache" not in content: + issues.append("Missing BuildKit cache mounts for optimization") + + # Check for distroless + if "distroless" not in content: + issues.append("Consider using distroless images for production") + + # Check for health check + if "HEALTHCHECK" not in content: + issues.append("Missing health check configuration") + + # Check for Python version pinning + python_from_lines = [ + line for line in lines if line.startswith("FROM") and "python:" in line + ] + for line in python_from_lines: + if "python:3-" in line or "python:latest" in line: + issues.append(f"Pin specific Python version: {line.strip()}") + + return issues + + +def validate_dockerignore(dockerignore_path: Path) -> list[str]: + """Validate .dockerignore completeness.""" + issues = [] + + if not dockerignore_path.exists(): + issues.append(".dockerignore file missing") + return issues + + content = dockerignore_path.read_text() + + # Essential patterns that should be ignored + essential_patterns = [ + "__pycache__", + (".git", "*.pyc", "*.py[cod]"), # Either *.pyc or *.py[cod] is fine + ".pytest_cache", + "docs/", + "*.log", + ] + + for pattern in essential_patterns: + if isinstance(pattern, tuple): + # Check if any of the alternatives exist + if not any(alt in content for alt in pattern): + issues.append( + f"Missing .dockerignore pattern (one of): {', '.join(pattern)}" + ) + else: + if pattern not in content: + issues.append(f"Missing .dockerignore pattern: {pattern}") + + return issues + + +def validate_compose_files(compose_paths: list[Path]) -> list[str]: + """Validate docker-compose files.""" + issues = [] + + for compose_path in compose_paths: + if not compose_path.exists(): + issues.append(f"Compose file missing: {compose_path}") + continue + + content = compose_path.read_text() + + # Check for version (should use modern format without version key) + if content.strip().startswith("version:"): + issues.append(f"{compose_path.name}: Remove deprecated 'version' key") + + # Check for named volumes + if "volumes:" not in content: + issues.append(f"{compose_path.name}: Consider using named volumes") + + # Check for health checks + if "healthcheck:" not in content: + issues.append(f"{compose_path.name}: Missing health checks") + + return issues + + +def main(): + """Main validation function.""" + print("๐Ÿณ Docker Setup Validation") + print("=" * 50) + + project_root = Path(__file__).parent + all_issues = [] + + # Validate Dockerfile + print("\n๐Ÿ“„ Validating Dockerfile...") + dockerfile_issues = validate_dockerfile(project_root / "Dockerfile") + if dockerfile_issues: + print("โš ๏ธ Issues found:") + for issue in dockerfile_issues: + print(f" - {issue}") + all_issues.extend(dockerfile_issues) + else: + print("โœ… Dockerfile looks good!") + + # Validate .dockerignore + print("\n๐Ÿšซ Validating .dockerignore...") + dockerignore_issues = validate_dockerignore(project_root / ".dockerignore") + if dockerignore_issues: + print("โš ๏ธ Issues found:") + for issue in dockerignore_issues: + print(f" - {issue}") + all_issues.extend(dockerignore_issues) + else: + print("โœ… .dockerignore looks good!") + + # Validate compose files + print("\n๐Ÿ™ Validating Docker Compose files...") + compose_files = [ + project_root / "docker-compose.yml", + project_root / "docker-compose.prod.yml", + ] + compose_issues = validate_compose_files(compose_files) + if compose_issues: + print("โš ๏ธ Issues found:") + for issue in compose_issues: + print(f" - {issue}") + all_issues.extend(compose_issues) + else: + print("โœ… Compose files look good!") + + # Summary + print("\n" + "=" * 50) + if all_issues: + print(f"โŒ Found {len(all_issues)} issues to address") + return 1 + else: + print("๐ŸŽ‰ All Docker configurations look great!") + print("\n๐Ÿ“š Usage Examples:") + print(" Development: docker-compose up") + print(" Testing: docker-compose --profile test up") + print(" Production: docker-compose -f docker-compose.prod.yml up") + return 0 + + +if __name__ == "__main__": + exit(main()) diff --git a/{{cookiecutter.project_slug}}/.github/workflows/check-code.yaml b/{{cookiecutter.project_slug}}/.github/workflows/check-code.yaml deleted file mode 100644 index 2d84fa5..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/check-code.yaml +++ /dev/null @@ -1,27 +0,0 @@ -{% raw %} -name: Check Code standards - -on: - push: - -jobs: - set-up-docker: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Install ruff - run: | - python -m pip install --upgrade pip - pip install ruff - - - name: run ruff - run: | - ruff check **/*.py - - - name: run ruff format - run: | - ruff format **/*.py -{% endraw %} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/release-docker-image.yaml b/{{cookiecutter.project_slug}}/.github/workflows/release-docker-image.yaml deleted file mode 100644 index c8661d9..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/release-docker-image.yaml +++ /dev/null @@ -1,45 +0,0 @@ -{% raw %} -name: Release Docker Image CI/CD - -on: - release: - types: - - published - - edited - -jobs: - set-up-docker: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Install docker - run: apt update && apt install -y docker-compose - - - name: Login to Docker registry - uses: docker/login-action@v3 - with: - registry: ${{ vars.DOCKER_REGISTRY_URL }} - username: ${{ secrets.DOCKER_REGISTRY_USERNAME }} - password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push release tag - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ vars.DOCKER_REGISTRY_URL }}/${{ github.event.repository.name }}:${{ github.ref_name }} - - - name: Build and push latest - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: ${{ vars.DOCKER_REGISTRY_URL }}/${{ github.event.repository.name }}:latest - -{% endraw %} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/test-docker-image.yaml b/{{cookiecutter.project_slug}}/.github/workflows/test-docker-image.yaml deleted file mode 100644 index 8e36a4e..0000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/test-docker-image.yaml +++ /dev/null @@ -1,27 +0,0 @@ -{% raw %} -name: Test Docker Image CI/CD - -on: - pull_request: - -jobs: - set-up-docker: - runs-on: ubuntu-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - - - name: Install docker - run: apt update && apt install -y docker-compose - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and test - uses: docker/build-push-action@v5 - with: - context: . - file: ./Dockerfile - push: false -{% endraw %} diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore deleted file mode 100644 index 594e8c4..0000000 --- a/{{cookiecutter.project_slug}}/.gitignore +++ /dev/null @@ -1,171 +0,0 @@ -.DS_Store -.coverage -.vscode/\n.idea/ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# Remove html generated by documentation - -docs/html - -# Remove tests and coverage reports -docs/cov-report -docs/pytest_report.html - -# 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 - -# 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 - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.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/ diff --git a/{{cookiecutter.project_slug}}/.opencode/agents/repo-manager.md b/{{cookiecutter.project_slug}}/.opencode/agents/repo-manager.md deleted file mode 100644 index 9a6671e..0000000 --- a/{{cookiecutter.project_slug}}/.opencode/agents/repo-manager.md +++ /dev/null @@ -1,193 +0,0 @@ ---- -description: Release Engineer managing Git workflows, pull requests, and hybrid calver releases with AI-themed naming -mode: subagent -temperature: 0.3 -tools: - write: false - edit: false - read: true - grep: true - glob: true - bash: true - task: true - skill: true -permission: - bash: - "git *": allow - "gh *": allow - "task *": allow - "*": ask ---- -You are a specialized Git repository management agent for {{cookiecutter.project_name}}. - -## Your Role -- Manage Git repository operations (commits, branches, merges) -- Create and manage pull requests using GitHub CLI -- Generate semantic releases with hybrid major.minor.calver versioning -- Create release names using adjective-animal themes based on PR sentiment analysis -- Maintain clean Git history and follow conventional commit standards - -## Version Format -Use hybrid versioning: `v{major}.{minor}.{YYYYMMDD}` - -**Examples:** -- `v1.2.20260302` - Version 1.2, release on March 2, 2026 -- `v1.3.20260313` - Version 1.3, release on March 13, 2026 -- `v1.4.20260313` - Version 1.4, second release same day -- `v2.0.20260401` - Version 2.0, release on April 1, 2026 - -**Version Rules:** -- **Major**: Increment for breaking changes -- **Minor**: Increment for new features (or same-day releases) -- **Date**: Release date YYYYMMDD - -## Release Naming Convention -Generate themed names using: `{adjective} {animal}` - -**Name Selection Strategy:** -1. Get merged PRs: `gh pr list --state merged --base main --limit 20` -2. **Use your AI to analyze** the PR titles and descriptions -3. Determine what this release is really about -4. Generate a unique adjective-animal name that: - - Reflects the PR content - - Hasn't been used before - - Is creative and memorable - -**Avoid** overused combinations like "swift cheetah", "creative fox", "vigilant owl", "innovative dolphin". - -**Try** unique combinations like: -- Exotic: narwhal, axolotl, capybara, quokka, pangolin -- Aquatic: jellyfish, seahorse, manta, cuttlefish, otter -- Birds: kingfisher, heron, ibis, stork -- Insects: firefly, butterfly, dragonfly -- Mythical: phoenix, griffin, pegasus, siren - -## Git Operations - -### Commit Standards -Follow conventional commits: -``` -(): - -[optional body] - -[optional footer(s)] -``` - -**Types**: feat, fix, docs, style, refactor, perf, test, build, ci, chore - -### Branch Management -- `main` - Production branch -- `develop` - Development branch -- `feature/*` - Feature branches -- `fix/*` - Bug fix branches -- `release/*` - Release preparation branches - -### PR Creation Workflow -1. Create feature branch from develop -2. Make commits following conventional commit format -3. Push branch and create PR using `gh pr create` -4. Add appropriate labels and reviewers -5. Merge after review and CI passes - -## Release Management - -### Release Process -1. **Prepare Release Branch** - ```bash - git checkout develop - git pull origin develop - git checkout -b release/v{version} - ``` - -2. **Analyze PR Sentiment** - - Use `gh pr list --state merged --base develop` - - Analyze PR titles/descriptions for themes - - Generate appropriate adjective-animal name - -3. **Update Version** - - Update `pyproject.toml` version field - - Update `CHANGELOG.md` with PR summaries - - Commit version bump - -4. **Create Release** - ```bash - git checkout main - git merge release/v{version} - git tag v{version} - git push origin main --tags - gh release create v{version} --title "{adjective} {animal}" --notes-from-tag - ``` - -5. **Sync Develop** - ```bash - git checkout develop - git merge main - git push origin develop - ``` - -## Available Skills -- **git-release**: Comprehensive release management with calver versioning -- **pr-management**: Pull request creation and management - -## Example Commands - -### Creating a Feature PR -```bash -git checkout -b feature/user-authentication -# ... make changes ... -git add . -git commit -m "feat(auth): add JWT authentication system" -git push origin feature/user-authentication -gh pr create --title "Add JWT Authentication" --body "Implements secure user authentication using JWT tokens" -``` - -### Creating a Release -```bash -# Analyze recent PRs for sentiment -gh pr list --state merged --base develop --limit 10 - -# Create release (example output) -# Recent PRs: "Optimize database queries", "Improve API performance", "Cache implementation" -# Theme detected: Performance improvements -# Generated name: "swift falcon" -# Version: v1.2.20260302 -``` - -### Emergency Hotfix -```bash -git checkout main -git checkout -b fix/critical-security-patch -# ... make fixes ... -git add . -git commit -m "fix(security): patch authentication vulnerability" -git push origin fix/critical-security-patch -gh pr create --title "Critical Security Patch" --body "Fixes authentication vulnerability" -# After merge, create immediate release with incremented revision -``` - -## Integration with Project Workflow - -### Pre-Release Checklist -- [ ] All tests pass: `task test` -- [ ] Linting passes: `task lint` -- [ ] Type checking passes: `task static-check` -- [ ] Documentation updated -- [ ] CHANGELOG.md updated -- [ ] Version bumped in pyproject.toml - -### Quality Gates -- Require PR reviews before merge -- Ensure CI passes on all PRs -- Run full test suite before releases -- Validate version format matches hybrid scheme -- Check release name follows adjective-animal format - -## Communication Style -- Provide clear Git commands with explanations -- Show before/after states for major operations -- Explain versioning decisions -- Suggest appropriate branch names and commit messages -- Give context for release naming choices - -You excel at maintaining clean Git history, creating meaningful releases, and ensuring proper repository management practices. diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/git-release/SKILL.md b/{{cookiecutter.project_slug}}/.opencode/skills/git-release/SKILL.md deleted file mode 100644 index d73bf57..0000000 --- a/{{cookiecutter.project_slug}}/.opencode/skills/git-release/SKILL.md +++ /dev/null @@ -1,307 +0,0 @@ ---- -name: git-release -description: Create semantic releases with hybrid major.minor.calver versioning and themed naming -license: MIT -compatibility: opencode -metadata: - audience: maintainers - workflow: release-management ---- -## What I do -Manage the complete release process including version calculation, changelog generation, release creation, and themed naming based on PR sentiment analysis. - -## When to use me -Use this when ready to create a new release after features are complete and tested. - -## Hybrid Versioning System - -### Version Format -`v{major}.{minor}.{YYYYMMDD}` - -**Components:** -- **Major**: Breaking changes (e.g., API changes, removed features) -- **Minor**: New features, significant enhancements, or same-day releases -- **Date**: Release date in YYYYMMDD format - -**Examples:** -``` -v1.0.20260302 # Version 1.0, release on March 2, 2026 -v1.1.20260315 # Version 1.1, release on March 15, 2026 -v1.2.20260315 # Version 1.2, second release same day -v2.0.20260401 # Version 2.0, breaking changes on April 1, 2026 -``` - -### Version Bump Rules -```bash -# Feature release (minor bump) -v1.2.20260302 โ†’ v1.3.{today} - -# Breaking change (major bump) -v1.2.20260302 โ†’ v2.0.{today} - -# Same day release (increment minor by 2) -v1.2.20260302 โ†’ v1.3.20260302 -``` - -## Release Naming Strategy - -### AI-Generated Themed Naming - -The release name is generated by the AI based on analyzing the merged PRs and their content. The AI should: - -1. **Analyze PR content** - Read titles, descriptions, and code changes -2. **Identify dominant theme** - What is this release really about? -3. **Generate unique name** - Create an adjective-animal pair that: - - Reflects the PR content accurately - - Has not been used before in this project - - Is memorable and appropriate - -### Guidelines for AI - -**Good adjectives** (avoid generic ones): -- Emotional/sensory: `electric`, `radiant`, `fiery`, `velvet`, `crystalline` -- Movement: `surging`, `drifting`, `soaring`, `diving`, `spiral` -- Nature: `blooming`, `misty`, `aurora`, `tidal`, `verdant` -- Abstract: `boundless`, `infinite`, `hidden`, `silent`, `luminous` - -**Good animals** (avoid overused ones like fox, owl, dolphin, cheetah): -- Exotic: `narwhal`, `axolotl`, `capybara`, `quokka`, `pangolin` -- Aquatic: `jellyfish`, `seahorse`, `manta`, `cuttlefish`, `otter` -- Birds: `kingfisher`, `heron`, `ibis`, `stork`, `bird-of-paradise` -- Insects: `firefly`, `butterfly`, `dragonfly`, `beetle`, `mantid` -- Mythical-inspired: `phoenix`, `griffin`, `pegasus`, `siren`, `chimera` - -**Avoid repetition** - Check previous releases and choose something new. - -### Examples - -Instead of pre-defined categories, the AI decides: -``` -v1.2.20260315 - Blooming Narwhal (based on new features, growth theme) -v1.3.20260316 - Velvet Manta (based on smooth refactoring) -v1.4.20260317 - Electric Firefly (based on performance improvements) -v1.5.20260403 - Crystal Jellyfish (based on documentation overhaul) -``` - -## PR Analysis for Release Naming - -### AI Analysis Process - -Instead of rigid keyword matching, the AI should: - -1. **Gather Recent PRs** - ```bash - gh pr list --state merged --base main --limit 20 --json title,body,labels - ``` - -2. **Understand the content** - Read PR titles and descriptions to understand what changed - -3. **Identify the story** - What's the narrative of this release? What stands out? - -4. **Generate creative name** - Based on understanding, create a unique adjective-animal pair - -**Remember:** -- Each release should have a unique name - don't repeat -- The name should feel authentic and memorable -- Let the PR content guide the naming, not the other way around - -## Release Process Workflow - -### Step 1: Prepare Release -```bash -# Ensure clean state -git checkout develop -git pull origin develop - -# Check for unreleased changes -git log --oneline $(git describe --tags --abbrev=0)..HEAD - -# Create release branch -current_date=$(date +%Y%m%d) -git checkout -b release/v1.3.${current_date} -``` - -### Step 2: Analyze PRs and Generate Name -```bash -# Get merged PRs since last release -last_tag=$(git describe --tags --abbrev=0) -gh pr list --state merged --base develop --limit 20 - -# Use YOUR AI to analyze the PR titles and descriptions: -# - Read each PR title and description -# - Understand what the changes are actually about -# - Determine the dominant theme/vibe -# - Select an appropriate adjective-animal pair - -# Example (you must do real analysis): -# Recent PRs: -# - "Add session-workflow skill for multi-session AI development" -# - "Remove BDD references and DEVELOPMENT_WORKFLOW.md" -# - "Template hotfix - Jinja2 escaping" -# -# AI Analysis: These are primarily about FEATURES and IMPROVEMENTS -# The addition of a new skill is the dominant theme -# Selected name: "creative fox" (features theme) -``` - -### Step 3: Update Version and Changelog -```bash -# Update pyproject.toml -sed -i 's/version = ".*"/version = "1.3.20260302"/' pyproject.toml - -# Generate changelog entry -cat >> CHANGELOG.md << EOF -## [v1.3.20260302] - Swift Cheetah - 2026-03-02 - -### Performance Improvements -- Optimize database query performance (#123) -- Add caching layer for API responses (#124) -- Improve search algorithm efficiency (#125) -- Speed up test suite execution (#126) - -### Migration Notes -- No breaking changes in this release -- Cache configuration is optional but recommended - -EOF - -# Commit version bump -git add pyproject.toml CHANGELOG.md -git commit -m "chore(release): bump version to v1.3.20260302 - Swift Cheetah" -``` - -### Step 4: Create and Publish Release -```bash -# Merge to main -git checkout main -git merge release/v1.3.20260302 - -# Create tag -git tag v1.3.20260302 - -# Push to remote -git push origin main --tags - -# Create GitHub release -gh release create v1.3.20260302 \ - --title "v1.3.20260302 - Swift Cheetah" \ - --notes-file CHANGELOG.md - -# Sync develop branch -git checkout develop -git merge main -git push origin develop - -# Clean up release branch -git branch -d release/v1.3.20260302 -git push origin --delete release/v1.3.20260302 -``` - -### Step 5: Post-Release Tasks -```bash -# Verify release -gh release view v1.3.20260302 - -# Check CI/CD pipeline -gh workflow run deploy --ref v1.3.20260302 - -# Update project documentation -echo "Latest release: v1.3.20260302 - Swift Cheetah" > .release-info -``` - -## Hotfix Release Process - -### Emergency Fixes (Same Day) -```bash -# Create hotfix from main -git checkout main -git checkout -b hotfix/critical-security-fix - -# Make minimal changes -git add . -git commit -m "fix(security): patch authentication vulnerability" - -# Create PR for review -gh pr create --title "Critical Security Hotfix" \ - --body "Emergency patch for authentication vulnerability" - -# After merge, create minor release for same day -current_date=$(date +%Y%m%d) -last_version=$(git describe --tags --abbrev=0) - -# Calculate next minor version (v1.3.20260302 โ†’ v1.4.20260302 if same day) -minor=$(echo $last_version | cut -d. -f2) -date_str=$(echo $last_version | cut -d. -f3) -new_minor=$((minor + 1)) -next_version="v${new_minor}.${date_str}" - -git tag $next_version -git push origin main --tags - -gh release create $next_version \ - --title "$next_version - Guardian Bear (Hotfix)" \ - --notes "Emergency security patch" -``` - -## Integration with Quality Pipeline - -### Pre-Release Validation -```bash -#!/bin/bash -# Release validation script - -echo "๐Ÿ” Running pre-release validation..." - -# Ensure all tests pass -task test || { echo "โŒ Tests failed"; exit 1; } - -# Verify linting -task lint || { echo "โŒ Linting failed"; exit 1; } - -# Check type safety -task static-check || { echo "โŒ Type checking failed"; exit 1; } - -# Validate version format -version=$(grep 'version =' pyproject.toml | cut -d'"' -f2) -if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]{8}$ ]]; then - echo "โŒ Invalid version format: $version" - exit 1 -fi - -# Check changelog updated -if ! grep -q $version CHANGELOG.md; then - echo "โŒ Version not found in CHANGELOG.md" - exit 1 -fi - -echo "โœ… Pre-release validation passed!" -``` - -## Example Release Scenarios - -### Feature Release -```bash -# Scenario: Added user dashboard, API improvements, new export feature -# Analysis: 3 feature PRs, 1 performance PR -# Theme: FEATURES (dominant) -# Name: "innovative dolphin" -# Version: v1.4.20260315 -``` - -### Security Release -```bash -# Scenario: Authentication fixes, permission updates, security audit -# Analysis: 4 security PRs, 1 docs PR -# Theme: SECURITY (dominant) -# Name: "vigilant owl" -# Version: v1.3.20260320 -``` - -### Major Release -```bash -# Scenario: API v2, removed legacy endpoints, new architecture -# Analysis: Breaking changes detected -# Theme: Based on supporting PRs -# Name: "pioneering eagle" -# Version: v2.0.20260401 -``` \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/.opencode/skills/pr-management/SKILL.md b/{{cookiecutter.project_slug}}/.opencode/skills/pr-management/SKILL.md deleted file mode 100644 index f4a9ad2..0000000 --- a/{{cookiecutter.project_slug}}/.opencode/skills/pr-management/SKILL.md +++ /dev/null @@ -1,512 +0,0 @@ ---- -name: pr-management -description: Create and manage pull requests with proper formatting, labels, and workflow integration -license: MIT -compatibility: opencode -metadata: - audience: developers - workflow: git-management ---- -## What I do -Streamline pull request creation and management with standardized formatting, automated labeling, and integration with the project's development workflow. - -## When to use me -Use this when creating PRs, managing code reviews, or handling PR lifecycle operations. - -## PR Creation Workflow - -### Branch Naming Conventions -```bash -# Feature branches -feature/user-authentication -feature/api-optimization -feature/dashboard-redesign - -# Bug fix branches -fix/authentication-bug -fix/performance-issue -fix/ui-alignment - -# Hotfix branches -hotfix/security-patch -hotfix/critical-bug - -# Documentation branches -docs/api-documentation -docs/setup-guide - -# Refactoring branches -refactor/auth-service -refactor/database-layer -``` - -### Conventional Commit Messages -```bash -# Format: (): -git commit -m "feat(auth): add JWT authentication system" -git commit -m "fix(api): resolve timeout issues in user endpoint" -git commit -m "docs(readme): update installation instructions" -git commit -m "refactor(db): simplify user query methods" -git commit -m "perf(cache): optimize Redis connection pooling" -git commit -m "test(auth): add integration tests for login flow" -``` - -**Commit Types:** -- `feat` - New features -- `fix` - Bug fixes -- `docs` - Documentation changes -- `style` - Code formatting (no logic changes) -- `refactor` - Code restructuring (no behavior changes) -- `perf` - Performance improvements -- `test` - Adding or updating tests -- `build` - Build system changes -- `ci` - CI/CD pipeline changes -- `chore` - Maintenance tasks - -### PR Template Structure -```markdown -## Summary -Brief description of what this PR accomplishes. - -## Type of Change -- [ ] ๐Ÿš€ New feature -- [ ] ๐Ÿ› Bug fix -- [ ] ๐Ÿ“š Documentation update -- [ ] ๐Ÿ”ง Refactoring -- [ ] โšก Performance improvement -- [ ] ๐Ÿงช Test addition/improvement -- [ ] ๐Ÿ”จ Build/CI changes - -## Changes Made -- Specific change 1 -- Specific change 2 -- Specific change 3 - -## Testing -- [ ] Unit tests added/updated -- [ ] Integration tests added/updated -- [ ] Manual testing completed -- [ ] All tests passing - -## Quality Checklist -- [ ] Code follows project style guidelines -- [ ] Self-review completed -- [ ] Documentation updated -- [ ] No console.log/print statements left -- [ ] Type hints added (Python) - -## Related Issues -Fixes #123 -Related to #456 - -## Screenshots/Examples -(If applicable) - -## Breaking Changes -(If any) - -## Migration Notes -(If applicable) -``` - -## PR Creation Commands - -### Standard Feature PR -```bash -# 1. Create and switch to feature branch -git checkout develop -git pull origin develop -git checkout -b feature/user-dashboard - -# 2. Make changes and commit -git add . -git commit -m "feat(dashboard): add user dashboard with analytics" - -# 3. Push branch -git push origin feature/user-dashboard - -# 4. Create PR with template -gh pr create \ - --title "Add User Dashboard with Analytics" \ - --body-file .github/pull_request_template.md \ - --label "feature" \ - --label "frontend" \ - --reviewer @team-leads \ - --assignee @me -``` - -### Bug Fix PR -```bash -git checkout -b fix/authentication-timeout -# ... make fixes ... -git commit -m "fix(auth): resolve JWT token timeout handling" -git push origin fix/authentication-timeout - -gh pr create \ - --title "Fix authentication timeout handling" \ - --body "Resolves issue where JWT tokens weren't properly refreshed on timeout" \ - --label "bug" \ - --label "security" \ - --reviewer @security-team -``` - -### Documentation PR -```bash -git checkout -b docs/api-examples -# ... update docs ... -git commit -m "docs(api): add comprehensive API usage examples" -git push origin docs/api-examples - -gh pr create \ - --title "Add comprehensive API usage examples" \ - --body "Enhances API documentation with practical examples and use cases" \ - --label "documentation" \ - --label "enhancement" -``` - -## PR Management Operations - -### Draft PR for Work in Progress -```bash -gh pr create \ - --title "[WIP] Implement user authentication" \ - --body "Work in progress - authentication system implementation" \ - --draft \ - --label "wip" - -# Convert draft to ready when complete -gh pr ready 123 -``` - -### PR Review Management -```bash -# Request review from specific users -gh pr edit 123 --add-reviewer @alice,@bob - -# Request review from teams -gh pr edit 123 --add-reviewer @team/backend-team - -# Add labels -gh pr edit 123 --add-label "priority:high" --add-label "needs-testing" - -# Update PR description -gh pr edit 123 --body "Updated description with new requirements" -``` - -### Automated PR Checks -```bash -# Check PR status -gh pr status - -# View PR checks -gh pr checks 123 - -# Wait for checks to complete -gh pr checks 123 --watch - -# View detailed check output -gh run view $(gh pr view 123 --json headRefOid --jq .headRefOid) -``` - -## PR Labels and Categories - -### Standard Labels -```yaml -# Type labels -- name: "feature" - color: "0e8a16" - description: "New feature or enhancement" - -- name: "bug" - color: "d73a4a" - description: "Bug fix" - -- name: "documentation" - color: "0075ca" - description: "Documentation changes" - -- name: "refactor" - color: "fbca04" - description: "Code refactoring" - -- name: "performance" - color: "ff6600" - description: "Performance improvements" - -# Priority labels -- name: "priority:critical" - color: "b60205" - description: "Critical priority" - -- name: "priority:high" - color: "d93f0b" - description: "High priority" - -- name: "priority:medium" - color: "fbca04" - description: "Medium priority" - -- name: "priority:low" - color: "0e8a16" - description: "Low priority" - -# Size labels -- name: "size:xs" - color: "c2e0c6" - description: "< 10 lines changed" - -- name: "size:s" - color: "7fcdcd" - description: "10-29 lines changed" - -- name: "size:m" - color: "bfd4f2" - description: "30-99 lines changed" - -- name: "size:l" - color: "d4c5f9" - description: "100-499 lines changed" - -- name: "size:xl" - color: "f9d0c4" - description: "500+ lines changed" -``` - -### Auto-Labeling Rules -```yaml -# .github/labeler.yml -"frontend": - - "frontend/**" - - "src/components/**" - - "**/*.tsx" - - "**/*.css" - -"backend": - - "backend/**" - - "src/api/**" - - "**/*.py" - -"documentation": - - "docs/**" - - "**/*.md" - - "README.md" - -"tests": - - "tests/**" - - "**/*test*" - - "**/*spec*" - -"ci": - - ".github/**" - - "Dockerfile" - - "docker-compose.yml" -``` - -## PR Integration with Development Workflow - -### Feature Development Flow -```bash -# 1. Start feature development -/skill feature-definition # Define requirements -/skill prototype-script # Create prototype -/skill tdd # Write tests - -# 2. Create PR for design review -git checkout -b feature/user-auth -git add tests/ -git commit -m "test(auth): add authentication test suite" -git push origin feature/user-auth - -gh pr create \ - --title "[DESIGN] Authentication system test suite" \ - --body "Test-driven design for authentication system. Ready for @architect review." \ - --label "design-review" \ - --label "tests" \ - --reviewer @architect \ - --draft - -# 3. After architect approval, implement -/skill signature-design # Design interfaces -/skill implementation # Implement features - -git add src/ -git commit -m "feat(auth): implement JWT authentication system" -git push origin feature/user-auth - -# 4. Mark ready for review -gh pr ready -gh pr edit --remove-label "design-review" --add-label "feature" -``` - -### Quality Gates Integration -```bash -# PR status checks (via GitHub Actions) -name: PR Quality Gates -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - quality: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Run tests - run: task test - - - name: Check linting - run: task lint - - - name: Type checking - run: task static-check - - - name: Coverage report - run: task test-report - - - name: Size labeling - uses: codelytv/pr-size-labeler@v1 -``` - -### Automated PR Workflows -```bash -# Auto-merge for dependency updates -gh pr merge 123 --auto --squash - -# Close stale PRs -gh pr list --state open --json number,updatedAt | \ - jq '.[] | select(.updatedAt < "2024-01-01") | .number' | \ - xargs -I {} gh pr close {} - -# Bulk label application -gh pr list --label "bug" --json number | \ - jq -r '.[].number' | \ - xargs -I {} gh pr edit {} --add-label "needs-testing" -``` - -## PR Review Guidelines - -### Review Checklist -```markdown -## Code Review Checklist - -### Functionality -- [ ] Code does what it's supposed to do -- [ ] Edge cases are handled -- [ ] Error handling is appropriate -- [ ] Performance implications considered - -### Code Quality -- [ ] Follows project coding standards -- [ ] SOLID principles applied -- [ ] Object calisthenics followed -- [ ] No code smells detected - -### Testing -- [ ] Adequate test coverage -- [ ] Tests are meaningful -- [ ] Test naming conventions followed -- [ ] Property-based tests where appropriate - -### Documentation -- [ ] Code is self-documenting -- [ ] Complex logic explained -- [ ] API documentation updated -- [ ] README updated if needed - -### Security -- [ ] No sensitive data exposed -- [ ] Input validation present -- [ ] Authentication/authorization correct -- [ ] No SQL injection risks -``` - -### Review Commands -```bash -# Approve PR -gh pr review 123 --approve --body "LGTM! Great work on the authentication system." - -# Request changes -gh pr review 123 --request-changes --body "Please address the security concerns mentioned inline." - -# Add review comments -gh pr comment 123 --body "Consider using a constant for the timeout value." - -# Check PR conversation -gh pr view 123 -``` - -## Emergency PR Procedures - -### Hotfix Process -```bash -# 1. Create hotfix from main -git checkout main -git pull origin main -git checkout -b hotfix/critical-security-fix - -# 2. Make minimal fix -git add . -git commit -m "fix(security): patch authentication vulnerability" - -# 3. Create emergency PR -gh pr create \ - --title "๐Ÿšจ CRITICAL: Security vulnerability patch" \ - --body "Emergency security fix - requires immediate review and merge" \ - --label "critical" \ - --label "security" \ - --label "hotfix" \ - --reviewer @security-team \ - --reviewer @team-leads - -# 4. Fast-track review process -gh pr merge --admin --squash -``` - -### Rollback PR -```bash -# Create rollback PR -git checkout main -git revert HEAD~1 # Revert last commit -git checkout -b fix/rollback-problematic-change - -gh pr create \ - --title "Rollback: Revert problematic authentication changes" \ - --body "Rolling back changes due to production issues" \ - --label "rollback" \ - --label "critical" -``` - -## Integration Examples - -### With Release Management -```bash -# After PR merge, check if release needed -merged_prs=$(gh pr list --state merged --base develop --limit 10) -if [[ $(echo "$merged_prs" | wc -l) -ge 5 ]]; then - echo "Consider creating release - 5+ PRs merged" - @repo-manager /skill git-release -fi -``` - -### With CI/CD Pipeline -{% raw %} -```yaml -# Auto-deployment for specific labels -name: Auto Deploy -on: - pull_request: - types: [closed] - -jobs: - deploy: - if: github.event.pull_request.merged && contains(github.event.pull_request.labels.*.name, 'deploy:staging') - runs-on: ubuntu-latest - steps: - - name: Deploy to staging - run: | - echo "Deploying PR #${{ github.event.pull_request.number }} to staging" - # Deployment commands... -``` -{% endraw %} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/Dockerfile b/{{cookiecutter.project_slug}}/Dockerfile deleted file mode 100644 index a28a606..0000000 --- a/{{cookiecutter.project_slug}}/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -ARG BUILDPLATFORM=linux/amd64 - -FROM --platform=$BUILDPLATFORM python:3-alpine as test - -WORKDIR /home/user/app - -ENV PATH=$PATH:/home/user/.local/bin - -RUN apk add --no-cache build-base linux-headers -RUN pip install --no-cache uv taskipy - -COPY ./ ./ - -RUN pip install '.[dev]' - -ARG TESTBUILD=True -ENV TESTBUILD=$TESTBUILD -RUN if [ "$TESTBUILD" = 'True' ]; then task lint; fi -RUN if [ "$TESTBUILD" = 'True' ]; then task test; fi - -RUN uv build --wheel --out-dir dist - -CMD ["task", "test"] - -FROM --platform=$BUILDPLATFORM python:3-alpine as prod - -RUN addgroup --system user && adduser --system user --ingroup user -USER user - -WORKDIR /home/user/app - -COPY --chown=user:user --from=test /home/user/app/dist dist - -RUN pip install --no-cache dist/*.whl - -CMD ["python", "-m", "{{cookiecutter.package_name}}.{{cookiecutter.module_name}}"] - diff --git a/{{cookiecutter.project_slug}}/LICENSE b/{{cookiecutter.project_slug}}/LICENSE deleted file mode 100644 index 05e670c..0000000 --- a/{{cookiecutter.project_slug}}/LICENSE +++ /dev/null @@ -1,105 +0,0 @@ -{% if cookiecutter.license == 'MIT' -%} -MIT License - -Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.full_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 cookiecutter.license == 'BSD_3_Clause' %} - -BSD License - -Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.full_name }} -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* 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. - -* 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. -{% elif cookiecutter.license == 'Apache_2.0' -%} -Apache Software License 2.0 - -Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.full_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 cookiecutter.license == 'GPL_3.0' -%} -GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - {{ cookiecutter.project_short_description }} - Copyright (C) {% now 'local', '%Y' %} {{ cookiecutter.full_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 . - -Also add information on how to contact you by electronic and paper mail. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. -{% elif cookiecutter.license == 'Proprietary' %} - Copyright (C) {% now 'local', '%Y' %} {{ cookiecutter.full_name }} all rights reserved. -{% endif %} diff --git a/{{cookiecutter.project_slug}}/main.py b/{{cookiecutter.project_slug}}/main.py deleted file mode 100644 index a5f2119..0000000 --- a/{{cookiecutter.project_slug}}/main.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Test main file.""" - -import logging -import fire - -logger = logging.getLogger(__name__) - -def set_logging(verbosity: int = 0) -> None: - mapping = { - 1: logging.WARNING, - 2: logging.INFO, - 3: logging.DEBUG, - } - level = mapping.get(verbosity, logging.ERROR) - logging.basicConfig( - level=level, - format="%(levelname)s - %(name)s: %(message)s" - ) - -def main(verbosity: int = 0): - """Run with --verbosity=N (0..3+)""" - set_logging(verbosity) - logger.debug("debug") - logger.info("info") - logger.warning("warning") - logger.error("error") - return "done" - -if __name__ == "__main__": - fire.Fire(main) diff --git a/{{cookiecutter.project_slug}}/tests/basic_test.py b/{{cookiecutter.project_slug}}/tests/basic_test.py deleted file mode 100644 index 7cc8f15..0000000 --- a/{{cookiecutter.project_slug}}/tests/basic_test.py +++ /dev/null @@ -1,24 +0,0 @@ -{%- if cookiecutter.include_examples == "true" -%}from hypothesis import given, example, strategies as st -import math - -from {{cookiecutter.package_name}} import {{cookiecutter.module_name}} as m - - -@example(a=6, b=3) # result = 2 -@example(a=-8, b=2) # result = -4 -@example(a=0, b=5) # zero dividend -@example(a=579, b=9105) # the earlier failing example (float rounding) -@given( - a=st.integers(min_value=-10_000, max_value=10_000), - b=st.integers(min_value=-10_000, max_value=10_000).filter(lambda x: x != 0), -) -def test_divide_inverse(a: int, b: int) -> None: - """ - Given: Two integers a and b where b is non-zero - When: Calculator.divide(a, b) is called - Then: result * b should equal a (within float tolerance) - """ - result = m.Calculator.divide(a, b) - - assert math.isclose(result * b, a, rel_tol=1e-12, abs_tol=1e-12) -{% endif %} diff --git a/{{cookiecutter.project_slug}}/tests/conftest.py b/{{cookiecutter.project_slug}}/tests/conftest.py deleted file mode 100644 index 86ec9e2..0000000 --- a/{{cookiecutter.project_slug}}/tests/conftest.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Pytest configuration for BDD docstring display in HTML reports.""" - -import json -import os - -import pytest - - -def _build_docstring_map() -> dict[str, str]: - """Walk test files and map nodeid โ†’ docstring.""" - import ast - from pathlib import Path - - mapping: dict[str, str] = {} - tests_dir = Path(__file__).resolve().parent - project_root = tests_dir.parent - - for py_file in tests_dir.rglob("*_test.py"): - rel = str(py_file.relative_to(project_root)) - try: - tree = ast.parse(py_file.read_text()) - except (SyntaxError, OSError): - continue - - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef): - for item in node.body: - if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)): - if item.name.startswith("test_"): - doc = ast.get_docstring(item) - if doc: - mapping[f"{rel}::{node.name}::{item.name}"] = doc - elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): - if node.name.startswith("test_"): - doc = ast.get_docstring(node) - if doc: - mapping[f"{rel}::{node.name}"] = doc - - return mapping - - -@pytest.hookimpl(trylast=True) -def pytest_sessionfinish(session, exitstatus) -> None: - """Add docstrings to JSON and regenerate HTML.""" - html_output = session.config.getoption("--html-output") or "docs/tests" - json_report = session.config.getoption("--json-report") or "final_report.json" - json_path = os.path.join(html_output, json_report) - - try: - with open(json_path) as f: - data = json.load(f) - except (FileNotFoundError, json.JSONDecodeError): - return - - doc_map = _build_docstring_map() - for result in data.get("results", []): - nodeid = result.get("nodeid", "") - - # Strip parametrize suffix like [a-b] for lookup - if "[" in nodeid and nodeid.endswith("]"): - bracket_idx = nodeid.rindex("[") - base_nodeid = nodeid[:bracket_idx] - params = nodeid[bracket_idx + 1 : -1] - else: - base_nodeid = nodeid - params = None - - doc = doc_map.get(base_nodeid) - if doc: - doc_html = doc.replace("\n", "
") - if params: - param_doc = f"Params: ({params.replace('-', ', ')})\n{doc}" - param_html = param_doc.replace("\n", "
") - result["docstring"] = param_doc - result["test"] = param_html - else: - result["docstring"] = doc - result["test"] = doc_html - - with open(json_path, "w") as f: - json.dump(data, f, indent=2) - - from pytest_html_plus.generate_html_report import JSONReporter - - reporter = JSONReporter(json_path, "", html_output) - reporter.load_report() - reporter.generate_html_report() diff --git a/{{cookiecutter.project_slug}}/tests/{{cookiecutter.package_name}}_test.py b/{{cookiecutter.project_slug}}/tests/{{cookiecutter.package_name}}_test.py deleted file mode 100644 index 6d2ccf2..0000000 --- a/{{cookiecutter.project_slug}}/tests/{{cookiecutter.package_name}}_test.py +++ /dev/null @@ -1,102 +0,0 @@ -{%- if cookiecutter.include_examples == "true" -%}"""This file contains examples of how to write tests using pytest! - -Some good practices for writting great Python tests: - -Source: https://www.nerdwallet.com/blog/engineering/5-pytest-best-practices/ - - * Prefer mocker over mock - * Parametrize the same behavior, have different tests for different behaviors - * Don't modify fixture values in other fixtures - * Prefer responses over mocking outbound HTTP requests - * Prefer tmpdir over global test artifacts - -BDD Test Convention: - * Use descriptive naming: test__should_ - * All tests should have Given/When/Then docstrings - -""" - -from typing import Self - -import pytest - -from {{cookiecutter.package_name}} import {{cookiecutter.module_name}} as m - - -@pytest.mark.parametrize( - ("param1", "param2"), - [ - ("a", "b"), - ("c", "d"), - ], -) -class TestGroup: - """A class with common parameters, `param1` and `param2`.""" - - @pytest.fixture - def fixt(self: Self) -> int: - """This fixture will only be available within the scope of TestGroup. - - Returns: - int: A common value to be used by multiple tests - - """ - return 123 - - def test_one(self: Self, param1: str, param2: str, fixt: int) -> None: - """ - Given: Two different string parameters - When: Test executes - Then: Parameters should not be equal - """ - assert param1 != param2 - - -@pytest.mark.parametrize( - ("a", "b", "expected"), - [ - (1, 1, 1), - (42, 1, 42), - (84, 2, 42), - ], -) -def test_divide_ok(a: float, b: float, expected: float) -> None: - """ - Given: Valid division inputs - When: Calculator.divide(a, b) is called - Then: Should return expected result - """ - assert m.Calculator.divide(a, b) == expected - - -@pytest.mark.parametrize( - ("a", "b", "expected"), - [ - (42, "b", TypeError), - ("a", 42, TypeError), - (42, 0, ZeroDivisionError), - ], -) -def test_divide_error( - a: str | float, b: str | float, expected: float | Exception -) -> None: - """ - Given: Invalid division inputs - When: Calculator.divide(a, b) is called - Then: Should raise expected Exception - """ - with pytest.raises(expected): - m.Calculator.divide(a, b) -{%- elif cookiecutter.include_examples != "true" -%} -"""Pytest test module.""" - - -def test_basics() -> None: - """ - Given: A simple test - When: Test executes - Then: Should always pass - """ - assert True is True - -{% endif %} diff --git a/{{cookiecutter.project_slug}}/uv.lock b/{{cookiecutter.project_slug}}/uv.lock deleted file mode 100644 index 4c1695a..0000000 --- a/{{cookiecutter.project_slug}}/uv.lock +++ /dev/null @@ -1,786 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.13" - -[[package]] -name = "babel" -version = "2.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, -] - -[[package]] -name = "backrefs" -version = "5.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337 }, - { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142 }, - { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021 }, - { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915 }, - { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336 }, -] - -[[package]] -name = "bracex" -version = "2.5.post1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/6c/57418c4404cd22fe6275b8301ca2b46a8cdaa8157938017a9ae0b3edf363/bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6", size = 26641 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/02/8db98cdc1a58e0abd6716d5e63244658e6e63513c65f469f34b6f1053fd0/bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6", size = 11558 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, - { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, - { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, - { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, - { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, - { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, - { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, - { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, - { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, - { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, - { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, - { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, - { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, - { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -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 = "coverage" -version = "7.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/21/87e9b97b568e223f3438d93072479c2f36cc9b3f6b9f7094b9d50232acc0/coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd", size = 211708 }, - { url = "https://files.pythonhosted.org/packages/75/be/882d08b28a0d19c9c4c2e8a1c6ebe1f79c9c839eb46d4fca3bd3b34562b9/coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00", size = 211981 }, - { url = "https://files.pythonhosted.org/packages/7a/1d/ce99612ebd58082fbe3f8c66f6d8d5694976c76a0d474503fa70633ec77f/coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64", size = 245495 }, - { url = "https://files.pythonhosted.org/packages/dc/8d/6115abe97df98db6b2bd76aae395fcc941d039a7acd25f741312ced9a78f/coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067", size = 242538 }, - { url = "https://files.pythonhosted.org/packages/cb/74/2f8cc196643b15bc096d60e073691dadb3dca48418f08bc78dd6e899383e/coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008", size = 244561 }, - { url = "https://files.pythonhosted.org/packages/22/70/c10c77cd77970ac965734fe3419f2c98665f6e982744a9bfb0e749d298f4/coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733", size = 244633 }, - { url = "https://files.pythonhosted.org/packages/38/5a/4f7569d946a07c952688debee18c2bb9ab24f88027e3d71fd25dbc2f9dca/coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323", size = 242712 }, - { url = "https://files.pythonhosted.org/packages/bb/a1/03a43b33f50475a632a91ea8c127f7e35e53786dbe6781c25f19fd5a65f8/coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3", size = 244000 }, - { url = "https://files.pythonhosted.org/packages/6a/89/ab6c43b1788a3128e4d1b7b54214548dcad75a621f9d277b14d16a80d8a1/coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d", size = 214195 }, - { url = "https://files.pythonhosted.org/packages/12/12/6bf5f9a8b063d116bac536a7fb594fc35cb04981654cccb4bbfea5dcdfa0/coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487", size = 214998 }, - { url = "https://files.pythonhosted.org/packages/2a/e6/1e9df74ef7a1c983a9c7443dac8aac37a46f1939ae3499424622e72a6f78/coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25", size = 212541 }, - { url = "https://files.pythonhosted.org/packages/04/51/c32174edb7ee49744e2e81c4b1414ac9df3dacfcb5b5f273b7f285ad43f6/coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42", size = 212767 }, - { url = "https://files.pythonhosted.org/packages/e9/8f/f454cbdb5212f13f29d4a7983db69169f1937e869a5142bce983ded52162/coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502", size = 256997 }, - { url = "https://files.pythonhosted.org/packages/e6/74/2bf9e78b321216d6ee90a81e5c22f912fc428442c830c4077b4a071db66f/coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1", size = 252708 }, - { url = "https://files.pythonhosted.org/packages/92/4d/50d7eb1e9a6062bee6e2f92e78b0998848a972e9afad349b6cdde6fa9e32/coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4", size = 255046 }, - { url = "https://files.pythonhosted.org/packages/40/9e/71fb4e7402a07c4198ab44fc564d09d7d0ffca46a9fb7b0a7b929e7641bd/coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73", size = 256139 }, - { url = "https://files.pythonhosted.org/packages/49/1a/78d37f7a42b5beff027e807c2843185961fdae7fe23aad5a4837c93f9d25/coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a", size = 254307 }, - { url = "https://files.pythonhosted.org/packages/58/e9/8fb8e0ff6bef5e170ee19d59ca694f9001b2ec085dc99b4f65c128bb3f9a/coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883", size = 255116 }, - { url = "https://files.pythonhosted.org/packages/56/b0/d968ecdbe6fe0a863de7169bbe9e8a476868959f3af24981f6a10d2b6924/coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada", size = 214909 }, - { url = "https://files.pythonhosted.org/packages/87/e9/d6b7ef9fecf42dfb418d93544af47c940aa83056c49e6021a564aafbc91f/coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257", size = 216068 }, - { url = "https://files.pythonhosted.org/packages/59/f1/4da7717f0063a222db253e7121bd6a56f6fb1ba439dcc36659088793347c/coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7", size = 203435 }, -] - -[[package]] -name = "gherkin-official" -version = "29.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/d8/7a28537efd7638448f7512a0cce011d4e3bf1c7f4794ad4e9c87b3f1e98e/gherkin_official-29.0.0.tar.gz", hash = "sha256:dbea32561158f02280d7579d179b019160d072ce083197625e2f80a6776bb9eb", size = 32303 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/fc/b86c22ad3b18d8324a9d6fe5a3b55403291d2bf7572ba6a16efa5aa88059/gherkin_official-29.0.0-py3-none-any.whl", hash = "sha256:26967b0d537a302119066742669e0e8b663e632769330be675457ae993e1d1bc", size = 37085 }, -] - -[[package]] -name = "ghp-import" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034 }, -] - -[[package]] -name = "griffe" -version = "1.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/59/08/7df7e90e34d08ad890bd71d7ba19451052f88dc3d2c483d228d1331a4736/griffe-1.7.2.tar.gz", hash = "sha256:98d396d803fab3b680c2608f300872fd57019ed82f0672f5b5323a9ad18c540c", size = 394919 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/5e/38b408f41064c9fcdbb0ea27c1bd13a1c8657c4846e04dab9f5ea770602c/griffe-1.7.2-py3-none-any.whl", hash = "sha256:1ed9c2e338a75741fc82083fe5a1bc89cb6142efe126194cc313e34ee6af5423", size = 129187 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, -] - -[[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 = "mako" -version = "1.3.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509 }, -] - -[[package]] -name = "markdown" -version = "3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, -] - -[[package]] -name = "mergedeep" -version = "1.3.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354 }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mergedeep" }, - { name = "mkdocs-get-deps" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "pyyaml" }, - { name = "pyyaml-env-tag" }, - { name = "watchdog" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451 }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047 }, -] - -[[package]] -name = "mkdocs-awesome-pages-plugin" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mkdocs" }, - { name = "natsort" }, - { name = "wcmatch" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/e8/6ae9c18d8174a5d74ce4ade7a7f4c350955063968bc41ff1e5833cff4a2b/mkdocs_awesome_pages_plugin-2.10.1.tar.gz", hash = "sha256:cda2cb88c937ada81a4785225f20ef77ce532762f4500120b67a1433c1cdbb2f", size = 16303 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/73/61/19fc1e9c579dbfd4e8a402748f1d63cab7aabe8f8d91eb0235e45b32d040/mkdocs_awesome_pages_plugin-2.10.1-py3-none-any.whl", hash = "sha256:c6939dbea37383fc3cf8c0a4e892144ec3d2f8a585e16fdc966b34e7c97042a7", size = 15118 }, -] - -[[package]] -name = "mkdocs-gen-files" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/85/2d634462fd59136197d3126ca431ffb666f412e3db38fd5ce3a60566303e/mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc", size = 7539 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/0f/1e55b3fd490ad2cecb6e7b31892d27cb9fc4218ec1dab780440ba8579e74/mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea", size = 8380 }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mergedeep" }, - { name = "platformdirs" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521 }, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.11" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "babel" }, - { name = "backrefs" }, - { name = "colorama" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "mkdocs" }, - { name = "mkdocs-material-extensions" }, - { name = "paginate" }, - { name = "pygments" }, - { name = "pymdown-extensions" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/7e/c65e330e99daa5813e7594e57a09219ad041ed631604a72588ec7c11b34b/mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff", size = 3951595 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/19/91/79a15a772151aca0d505f901f6bbd4b85ee1fe54100256a6702056bab121/mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263", size = 8703720 }, -] - -[[package]] -name = "mkdocs-material-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728 }, -] - -[[package]] -name = "mkdocstrings" -version = "0.29.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, - { name = "mkdocs-autorefs" }, - { name = "pymdown-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/e8/d22922664a627a0d3d7ff4a6ca95800f5dde54f411982591b4621a76225d/mkdocstrings-0.29.1.tar.gz", hash = "sha256:8722f8f8c5cd75da56671e0a0c1bbed1df9946c0cef74794d6141b34011abd42", size = 1212686 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075 }, -] - -[package.optional-dependencies] -python = [ - { name = "mkdocstrings-python" }, -] - -[[package]] -name = "mkdocstrings-python" -version = "1.16.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/c8/600c4201b6b9e72bab16802316d0c90ce04089f8e6bb5e064cd2a5abba7e/mkdocstrings_python-1.16.10.tar.gz", hash = "sha256:f9eedfd98effb612ab4d0ed6dd2b73aff6eba5215e0a65cea6d877717f75502e", size = 205771 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/37/19549c5e0179785308cc988a68e16aa7550e4e270ec8a9878334e86070c6/mkdocstrings_python-1.16.10-py3-none-any.whl", hash = "sha256:63bb9f01f8848a644bdb6289e86dc38ceddeaa63ecc2e291e3b2ca52702a6643", size = 124112 }, -] - -[[package]] -name = "mslex" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/97/7022667073c99a0fe028f2e34b9bf76b49a611afd21b02527fbfd92d4cd5/mslex-1.3.0.tar.gz", hash = "sha256:641c887d1d3db610eee2af37a8e5abda3f70b3006cdfd2d0d29dc0d1ae28a85d", size = 11583 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/f2/66bd65ca0139675a0d7b18f0bada6e12b51a984e41a76dbe44761bf1b3ee/mslex-1.3.0-py3-none-any.whl", hash = "sha256:c7074b347201b3466fc077c5692fbce9b5f62a63a51f537a53fbbd02eff2eea4", size = 7820 }, -] - -[[package]] -name = "natsort" -version = "8.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e2/a9/a0c57aee75f77794adaf35322f8b6404cbd0f89ad45c87197a937764b7d0/natsort-8.4.0.tar.gz", hash = "sha256:45312c4a0e5507593da193dedd04abb1469253b601ecaf63445ad80f0a1ea581", size = 76575 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/82/7a9d0550484a62c6da82858ee9419f3dd1ccc9aa1c26a1e43da3ecd20b0d/natsort-8.4.0-py3-none-any.whl", hash = "sha256:4732914fb471f56b5cce04d7bae6f164a592c7712e1c85f9ef585e197299521c", size = 38268 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "paginate" -version = "0.5.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746 }, -] - -[[package]] -name = "parse" -version = "1.20.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/78/d9b09ba24bb36ef8b83b71be547e118d46214735b6dfb39e4bfde0e9b9dd/parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce", size = 29391 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/31/ba45bf0b2aa7898d81cbbfac0e88c267befb59ad91a19e36e1bc5578ddb1/parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558", size = 20126 }, -] - -[[package]] -name = "parse-type" -version = "0.6.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parse" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/e9/a3b2ae5f8a852542788ac1f1865dcea0c549cc40af243f42cabfa0acf24d/parse_type-0.6.4.tar.gz", hash = "sha256:5e1ec10440b000c3f818006033372939e693a9ec0176f446d9303e4db88489a6", size = 96480 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/b3/f6cc950042bfdbe98672e7c834d930f85920fb7d3359f59096e8d2799617/parse_type-0.6.4-py2.py3-none-any.whl", hash = "sha256:83d41144a82d6b8541127bf212dd76c7f01baff680b498ce8a4d052a7a5bce4c", size = 27442 }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/2d/7d512a3913d60623e7eb945c6d1b4f0bddf1d0b7ada5225274c87e5b53d1/platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351", size = 21291 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "psutil" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/5a/07871137bb752428aa4b659f910b399ba6f291156bdea939be3e96cae7cb/psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5", size = 508502 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/99/ca79d302be46f7bdd8321089762dd4476ee725fce16fc2b2e1dbba8cac17/psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8", size = 247511 }, - { url = "https://files.pythonhosted.org/packages/0b/6b/73dbde0dd38f3782905d4587049b9be64d76671042fdcaf60e2430c6796d/psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377", size = 248985 }, - { url = "https://files.pythonhosted.org/packages/17/38/c319d31a1d3f88c5b79c68b3116c129e5133f1822157dd6da34043e32ed6/psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003", size = 284488 }, - { url = "https://files.pythonhosted.org/packages/9c/39/0f88a830a1c8a3aba27fededc642da37613c57cbff143412e3536f89784f/psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160", size = 287477 }, - { url = "https://files.pythonhosted.org/packages/47/da/99f4345d4ddf2845cb5b5bd0d93d554e84542d116934fde07a0c50bd4e9f/psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3", size = 289017 }, - { url = "https://files.pythonhosted.org/packages/38/53/bd755c2896f4461fd4f36fa6a6dcb66a88a9e4b9fd4e5b66a77cf9d4a584/psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53", size = 250602 }, - { url = "https://files.pythonhosted.org/packages/7b/d7/7831438e6c3ebbfa6e01a927127a6cb42ad3ab844247f3c5b96bea25d73d/psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649", size = 254444 }, -] - -[[package]] -name = "pygments" -version = "2.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7c/44/e6de2fdc880ad0ec7547ca2e087212be815efbc9a425a8d5ba9ede602cbb/pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b", size = 846846 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/f5/b9e2a42aa8f9e34d52d66de87941ecd236570c7ed2e87775ed23bbe4e224/pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9", size = 264467 }, -] - -[[package]] -name = "pytest" -version = "8.3.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, -] - -[[package]] -name = "pytest-bdd" -version = "8.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gherkin-official" }, - { name = "mako" }, - { name = "packaging" }, - { name = "parse" }, - { name = "parse-type" }, - { name = "pytest" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2d/2f/14c2e55372a5718a93b56aea48cd6ccc15d2d245364e516cd7b19bbd07ad/pytest_bdd-8.1.0.tar.gz", hash = "sha256:ef0896c5cd58816dc49810e8ff1d632f4a12019fb3e49959b2d349ffc1c9bfb5", size = 56147 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/7d/1461076b0cc9a9e6fa8b51b9dea2677182ba8bc248d99d95ca321f2c666f/pytest_bdd-8.1.0-py3-none-any.whl", hash = "sha256:2124051e71a05ad7db15296e39013593f72ebf96796e1b023a40e5453c47e5fb", size = 49149 }, -] - -[[package]] -name = "pytest-cov" -version = "6.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, -] - -[[package]] -name = "pytest-html" -version = "4.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "pytest" }, - { name = "pytest-metadata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491 }, -] - -[[package]] -name = "pytest-metadata" -version = "3.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-project-example" -version = "0.1.0" -source = { virtual = "." } - -[package.optional-dependencies] -dev = [ - { name = "mkdocs" }, - { name = "mkdocs-awesome-pages-plugin" }, - { name = "mkdocs-gen-files" }, - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, - { name = "pytest" }, - { name = "pytest-bdd" }, - { name = "pytest-cov" }, - { name = "pytest-html" }, - { name = "ruff" }, - { name = "taskipy" }, -] - -[package.metadata] -requires-dist = [ - { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6.1" }, - { name = "mkdocs-awesome-pages-plugin", marker = "extra == 'dev'", specifier = ">=2.10.1" }, - { name = "mkdocs-gen-files", marker = "extra == 'dev'", specifier = ">=0.5.0" }, - { name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.6.11" }, - { name = "mkdocstrings", extras = ["python"], marker = "extra == 'dev'", specifier = ">=0.29.1" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, - { name = "pytest-bdd", marker = "extra == 'dev'", specifier = ">=8.1.0" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" }, - { name = "pytest-html", marker = "extra == 'dev'", specifier = ">=4.1.1" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.5" }, - { name = "taskipy", marker = "extra == 'dev'", specifier = ">=1.14.1" }, -] -provides-extras = ["dev"] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8e/da1c6c58f751b70f8ceb1eb25bc25d524e8f14fe16edcce3f4e3ba08629c/pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb", size = 5631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/66/bbb1dd374f5c870f59c5bb1db0e18cbe7fa739415a24cbd95b2d1f5ae0c4/pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069", size = 3911 }, -] - -[[package]] -name = "requests" -version = "2.32.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, -] - -[[package]] -name = "ruff" -version = "0.11.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, - { url = "https://files.pythonhosted.org/packages/44/f2/06cd9006077a8db61956768bc200a8e52515bf33a8f9b671ee527bb10d77/ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077", size = 10898637 }, - { url = "https://files.pythonhosted.org/packages/18/f5/af390a013c56022fe6f72b95c86eb7b2585c89cc25d63882d3bfe411ecf1/ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779", size = 10236012 }, - { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, - { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, - { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, - { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, - { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, - { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, - { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, - { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, - { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, - { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, - { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, - { url = "https://files.pythonhosted.org/packages/fc/52/047f35d3b20fd1ae9ccfe28791ef0f3ca0ef0b3e6c1a58badd97d450131b/ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe", size = 10320697 }, - { url = "https://files.pythonhosted.org/packages/b9/fe/00c78010e3332a6e92762424cf4c1919065707e962232797d0b57fd8267e/ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800", size = 11378665 }, - { url = "https://files.pythonhosted.org/packages/43/7c/c83fe5cbb70ff017612ff36654edfebec4b1ef79b558b8e5fd933bab836b/ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e", size = 10460287 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "taskipy" -version = "1.14.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, - { name = "mslex", marker = "sys_platform == 'win32'" }, - { name = "psutil" }, - { name = "tomli", marker = "python_full_version < '4.0'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/44/572261df3db9c6c3332f8618fafeb07a578fd18b06673c73f000f3586749/taskipy-1.14.1.tar.gz", hash = "sha256:410fbcf89692dfd4b9f39c2b49e1750b0a7b81affd0e2d7ea8c35f9d6a4774ed", size = 14475 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/97/4e4cfb1391c81e926bebe3d68d5231b5dbc3bb41c6ba48349e68a881462d/taskipy-1.14.1-py3-none-any.whl", hash = "sha256:6e361520f29a0fd2159848e953599f9c75b1d0b047461e4965069caeb94908f1", size = 13052 }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, -] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, -] - -[[package]] -name = "urllib3" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, -] - -[[package]] -name = "watchdog" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480 }, - { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451 }, - { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057 }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079 }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076 }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077 }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078 }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065 }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070 }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067 }, -] - -[[package]] -name = "wcmatch" -version = "10.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "bracex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/ab/b3a52228538ccb983653c446c1656eddf1d5303b9cb8b9aef6a91299f862/wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a", size = 115578 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/df/4ee467ab39cc1de4b852c212c1ed3becfec2e486a51ac1ce0091f85f38d7/wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a", size = 39347 }, -] diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py b/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py deleted file mode 100644 index 318e8ab..0000000 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""{{cookiecutter.project_short_description}}.""" diff --git a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/{{cookiecutter.module_name}}.py b/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/{{cookiecutter.module_name}}.py deleted file mode 100644 index 9c8f983..0000000 --- a/{{cookiecutter.project_slug}}/{{cookiecutter.package_name}}/{{cookiecutter.module_name}}.py +++ /dev/null @@ -1,56 +0,0 @@ -{%- if cookiecutter.include_examples == "true" -%} -"""Module Docstring.""" - -import logging - -# TODO({{cookiecutter.full_name}}): Check how to write todos! -# https://docs.astral.sh/ruff/rules/missing-todo-link/ - -logger = logging.getLogger("{{cookiecutter.module_name}}") -logger.info("This is a {word}", extra={"word": "Log"}) - - -class Calculator: - """Class for simple calculator operations.""" - - @staticmethod - def divide(a: float, b: float) -> float: - """Divide a by b. - - Args: - a (float): Dividend. - b (float): Divisor. - - Returns: - float: The result of the division. - - Raises: - ZeroDivisionError: if b is 0. - TypeError: if a or b are not float numbers. - - Examples: - You can run this function as following. - - >>> Calculator.divide(2,1) - 2.0 - - """ - if b == 0: - raise ZeroDivisionError - if type(a) not in (float, int) or type(b) not in (float, int): - raise TypeError - return a / b - - -if __name__ == "__main__": - logger.warning("RUNNING!") -{%- elif cookiecutter.include_examples != "true" -%} -"""Module Docstring.""" - -import logging - -logger = logging.getLogger("{{cookiecutter.module_name}}") - - -if __name__ == "__main__": - logger.warning("RUNNING!"){% endif %}