From b6ceb2c1c48cbb0f443ced3dfece4a40e46dff2d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 16:03:25 +0000 Subject: [PATCH 1/4] =?UTF-8?q?Implement=20Claude=20PR=20BugScan=20workflo?= =?UTF-8?q?w=20(Draft=E2=86=92Ready=20+=20push=20re-scan)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strips the Python project template cruft and replaces it with a focused GitHub Actions setup for automated Claude PR code review: - .github/workflows/claude_pr_review.yml: reusable workflow_call that checks out both the target repo and this workflow repo, loads the Codex Code Review prompt, and runs anthropics/claude-code-action@v1 - .github/workflows/ai_pr_review.yml: caller workflow triggered on ready_for_review / synchronize / reopened, with draft guard and concurrency cancel-in-progress - prompts/codex_code_review_prompt.md: verbatim official Codex Code Review prompt from developers.openai.com cookbook - README.md: usage instructions, trigger table, versioning guidance - .gitignore: trimmed to repo-relevant patterns only https://claude.ai/code/session_018moumKtzZQiSiyGxVUFGdS --- .actrc | 11 - .github/workflows/ai_pr_review.yml | 20 + .github/workflows/ci-debug.yml | 44 - .github/workflows/claude_pr_review.yml | 55 + .github/workflows/docs.yml | 82 -- .github/workflows/tests.yml | 52 - .gitignore | 26 +- .pre-commit-config.yaml | 27 - CHANGELOG.md | 40 - CONTRIBUTING.md | 41 - Makefile | 310 ------ README.md | 233 ++-- docker/Dockerfile | 28 - docker/Makefile | 61 -- docker/docker-compose.yml | 17 - docker/template.env | 15 - docs/container-setup.md | 138 --- docs/getting-started.md | 143 --- docs/index.md | 67 -- docs/reference/api.md | 9 - mkdocs.yml | 80 -- prompts/codex_code_review_prompt.md | 1 + pyproject.toml | 78 -- requirements-dev.txt | 46 - requirements.txt | 2 - scripts/init_project.py | 389 ------- src/__init__.py | 0 src/example.py | 35 - templates/.github/workflows/docs.yml.template | 82 -- templates/docs/getting-started.md.template | 100 -- templates/docs/index.md.template | 85 -- templates/docs/reference/api.md.template | 9 - templates/mkdocs.yml.template | 80 -- tests/__init__.py | 0 tests/test_docs_setup.py | 136 --- tests/test_example.py | 13 - uv.lock | 998 ------------------ 37 files changed, 149 insertions(+), 3404 deletions(-) delete mode 100644 .actrc create mode 100644 .github/workflows/ai_pr_review.yml delete mode 100644 .github/workflows/ci-debug.yml create mode 100644 .github/workflows/claude_pr_review.yml delete mode 100644 .github/workflows/docs.yml delete mode 100644 .github/workflows/tests.yml delete mode 100644 .pre-commit-config.yaml delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 Makefile delete mode 100644 docker/Dockerfile delete mode 100644 docker/Makefile delete mode 100644 docker/docker-compose.yml delete mode 100644 docker/template.env delete mode 100644 docs/container-setup.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/index.md delete mode 100644 docs/reference/api.md delete mode 100644 mkdocs.yml create mode 100644 prompts/codex_code_review_prompt.md delete mode 100644 pyproject.toml delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt delete mode 100755 scripts/init_project.py delete mode 100644 src/__init__.py delete mode 100644 src/example.py delete mode 100644 templates/.github/workflows/docs.yml.template delete mode 100644 templates/docs/getting-started.md.template delete mode 100644 templates/docs/index.md.template delete mode 100644 templates/docs/reference/api.md.template delete mode 100644 templates/mkdocs.yml.template delete mode 100644 tests/__init__.py delete mode 100644 tests/test_docs_setup.py delete mode 100644 tests/test_example.py delete mode 100644 uv.lock diff --git a/.actrc b/.actrc deleted file mode 100644 index 55d4656..0000000 --- a/.actrc +++ /dev/null @@ -1,11 +0,0 @@ -# Runner image configuration - use maintained catthehacker images --P ubuntu-latest=catthehacker/ubuntu:act-22.04 - -# Container architecture (explicit for M1/M2 Macs) ---container-architecture=linux/amd64 - -# Container daemon socket (will be overridden by Makefile) ---container-daemon-socket=- - -# Verbose output by default ---verbose diff --git a/.github/workflows/ai_pr_review.yml b/.github/workflows/ai_pr_review.yml new file mode 100644 index 0000000..b90cff4 --- /dev/null +++ b/.github/workflows/ai_pr_review.yml @@ -0,0 +1,20 @@ +name: Claude PR scan (Draft->Ready + Push) + +on: + pull_request: + types: [ready_for_review, synchronize, reopened] + +concurrency: + group: claude-pr-scan-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + scan: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # Optional: override prompt per repo: + # with: + # prompt_override: | + # diff --git a/.github/workflows/ci-debug.yml b/.github/workflows/ci-debug.yml deleted file mode 100644 index 633dfb6..0000000 --- a/.github/workflows/ci-debug.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: CI Debug (Local Testing) - -# This workflow is designed for fast local iteration with act -# It runs a subset of checks for quick feedback -# -# Usage: make ci-debug -# -# Customize the steps below for the specific tests you're working on - -on: - workflow_dispatch: - push: - branches: - - '**' - -jobs: - quick-check: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - - name: Install uv - run: pip install uv - - - name: Install dependencies - run: | - uv venv - uv sync --all-extras - - # Customize these steps based on what you're debugging - - name: Run Linter - run: uv run -m ruff check src - - - name: Run Type Checker - run: uv run ty check src - - # Add more steps as needed for debugging - # - name: Run Specific Test - # run: uv run -m pytest tests/test_specific.py -v diff --git a/.github/workflows/claude_pr_review.yml b/.github/workflows/claude_pr_review.yml new file mode 100644 index 0000000..e6992e9 --- /dev/null +++ b/.github/workflows/claude_pr_review.yml @@ -0,0 +1,55 @@ +name: Claude PR review (Codex prompt) + +on: + workflow_call: + inputs: + prompt_override: + type: string + required: false + default: "" + secrets: + ANTHROPIC_API_KEY: + required: true + +jobs: + review: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write + + steps: + # Checkout the *target repository* (the caller repo) so Claude can see the code/diff context. + - name: Checkout target repo + uses: actions/checkout@v4 + + # Checkout this workflow repo to read the prompt file. + - name: Checkout workflow repo (for prompt) + uses: actions/checkout@v4 + with: + repository: safurrier/python-collab-template + path: _ai_workflows + + - name: Load prompt + id: prompt + shell: bash + run: | + if [ -n "${{ inputs.prompt_override }}" ]; then + echo "text<> "$GITHUB_OUTPUT" + printf "%s\n" "${{ inputs.prompt_override }}" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + else + echo "text<> "$GITHUB_OUTPUT" + cat _ai_workflows/prompts/codex_code_review_prompt.md >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Run Claude review + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: ${{ steps.prompt.outputs.text }} + # Optional knobs (uncomment if desired): + # use_sticky_comment: "true" + # claude_args: "--max-turns 5" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 2668105..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: Deploy Documentation - -on: - push: - branches: - - main - paths: - - 'docs/**' - - 'mkdocs.yml' - - '.github/workflows/docs.yml' - pull_request: - branches: - - main - paths: - - 'docs/**' - - 'mkdocs.yml' - -permissions: - contents: write - -jobs: - deploy: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install uv - run: pip install uv - - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - - uses: actions/cache@v4 - with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache - restore-keys: | - mkdocs-material- - - - name: Install dependencies - run: uv sync --group dev - - - name: Deploy documentation - run: uv run mkdocs gh-deploy --force - - build-check: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install uv - run: pip install uv - - - name: Install dependencies - run: uv sync --group dev - - - name: Build documentation - run: uv run mkdocs build --strict - - - name: Check build output - run: | - echo "βœ… Documentation builds successfully" - echo "πŸ“Š Site size: $(du -sh site/ | cut -f1)" - echo "πŸ“„ Pages built: $(find site/ -name "*.html" | wc -l)" \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 8df2d81..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Code Quality Checks - -on: - pull_request: - -jobs: - checks: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.12' - - - name: Install uv - run: pip install uv - - - name: Install dependencies - run: | - uv venv - uv sync --all-extras - - - name: Determine module name - id: module - run: | - if [ -d "src" ]; then - echo "name=src" >> $GITHUB_OUTPUT - else - MODULE_NAME=$(basename $(find . -maxdepth 1 -type d -not -path "*/\.*" -not -path "./tests" -not -path "./scripts" -not -path "./docker" -not -path "." | sort | head -1)) - echo "name=$MODULE_NAME" >> $GITHUB_OUTPUT - fi - - - name: Run Linter - run: uv run -m ruff check --fix ${{ steps.module.outputs.name }} - - - name: Run Formatter - run: uv run -m ruff format ${{ steps.module.outputs.name }} - - - name: Run Tests - run: uv run -m pytest tests --cov=${{ steps.module.outputs.name }} --cov-report=term-missing --cov-report=xml - - - name: Run ty - run: uv run ty check ${{ steps.module.outputs.name }} - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - continue-on-error: true - with: - files: coverage.xml - fail_ci_if_error: false diff --git a/.gitignore b/.gitignore index ff26fa8..d514899 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,14 @@ .DS_Store .idea/ -.venv/ -_build/ -notebooks/ -setup -conf.py -index.rst -.coverage -*.pyc -setup_mac.sh -docker/.env .vs_code/* -.mypy_cache/* -.pytest_cache/* -.ruff_cache/* .aider* CLAUDE.md -# Documentation build artifacts -site/ -.cache/ - -# AI/planning artifacts -.ai/** - -# act (GitHub Actions local runner) +# GitHub Actions local runner (act) +.actrc .actrc.local .secrets .env.act + +# AI/planning artifacts +.ai/** diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index c949a29..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,27 +0,0 @@ -repos: -- repo: local - hooks: - - id: lint - name: Run Linter - entry: make lint - language: system - always_run: true - pass_filenames: false - - id: format - name: Run Formatter - entry: make format - language: system - always_run: true - pass_filenames: false - - id: test - name: Run Tests - entry: make test - language: system - always_run: true - pass_filenames: false - - id: ty - name: Run ty - entry: make ty - language: system - always_run: true - pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2569156..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,40 +0,0 @@ -# Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## [Unreleased] - -### Added -- Integration with uv for dependency management -- Modern Python development tools: - - ruff for linting and formatting - - ty for type checking - - pytest with coverage reporting -- GitHub Actions workflow for automated testing -- Docker development environment improvements -- Local CI testing with act for running GitHub Actions workflows locally -- Fast debug workflow for iterative development -- Make targets: `act-install`, `ci-list`, `ci-local`, `ci-local-docs`, `ci-debug`, `ci-clean` - -### Changed -- Switched from pip/venv to uv for environment management -- Updated example code to pass ty type checking -- Modernized project structure and development workflow -- Updated Python version to 3.12 - -### Removed -- Legacy dependency management approach -- Outdated Docker configuration elements - -### Fixed -- Type hints in example code to pass ty checks -- Docker environment management -- Development workflow and quality checks - -## [0.1.0] - 2024-04-14 -- Initial fork from eugeneyan/python-collab-template -- Added Docker environment management -- Setup package installation configuration diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 9adc137..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,41 +0,0 @@ -# Contributing to Python Collab Template - -Thank you for your interest in contributing to this project! - -## Getting Started - -1. Fork the repository -2. Clone your fork: `git clone git@github.com:your-username/python-collab-template.git` -3. Create a new branch: `git checkout -b feature-name` -4. Make your changes -5. Run quality checks: `make check` -6. Commit your changes: `git commit -m "Description of changes"` -7. Push to your fork: `git push origin feature-name` -8. Open a Pull Request - -## Development Setup - -```bash -# Install dependencies and set up environment -make setup - -# Run all quality checks -make check -``` - -## Code Quality Standards - -- All code must be typed with proper type hints -- Tests must be included for new features -- Documentation must be updated when necessary -- All quality checks must pass (`make check`) - -## Pull Request Process - -1. Update the README.md with details of significant changes -2. Update the CHANGELOG.md following the existing format -3. The PR will be merged once you have the sign-off of at least one maintainer - -## Questions? - -Feel free to open an issue for any questions or concerns. diff --git a/Makefile b/Makefile deleted file mode 100644 index 9446886..0000000 --- a/Makefile +++ /dev/null @@ -1,310 +0,0 @@ -.PHONY: compile-deps setup clean-pyc clean-test clean-venv clean test ty lint format check clean-example docs-install docs-build docs-serve docs-check docs-clean dev-env refresh-containers rebuild-images build-image push-image - -# Module name - will be updated by init script -MODULE_NAME := src - -# Development Setup -################# -compile-deps: # Compile dependencies from pyproject.toml - uv pip compile pyproject.toml -o requirements.txt - uv pip compile pyproject.toml --extra dev -o requirements-dev.txt - -PYTHON_VERSION ?= 3.12 - -ensure-uv: # Install uv if not present - @which uv > /dev/null || (curl -LsSf https://astral.sh/uv/install.sh | sh) - -setup: ensure-uv ensure-scripts # Install dependencies - UV_PYTHON_VERSION=$(PYTHON_VERSION) uv sync --all-extras - $(MAKE) install-hooks - -install-hooks: # Install pre-commit hooks if in a git repo with hooks configured - @if [ -d .git ] && [ -f .pre-commit-config.yaml ]; then \ - echo "Installing pre-commit hooks..."; \ - uv run pre-commit install; \ - fi - -ensure-scripts: # Ensure scripts directory exists and files are executable - mkdir -p scripts - chmod +x scripts/*.py - -# Cleaning -######### -clean-pyc: # Remove Python compilation artifacts - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: # Remove test and coverage artifacts - rm -f .coverage - rm -f .coverage.* - -clean-venv: # Remove virtual environment - rm -rf .venv - -clean: clean-pyc clean-test clean-venv - -# Testing and Quality Checks -######################### -test: setup # Run pytest with coverage - uv run -m pytest tests --cov=$(MODULE_NAME) --cov-report=term-missing - -ty: setup # Run type checking - uv run ty check $(MODULE_NAME) - -lint: setup # Run ruff linter with auto-fix - uv run -m ruff check --fix $(MODULE_NAME) - -format: setup # Run ruff formatter - uv run -m ruff format $(MODULE_NAME) - -check: setup lint format test ty # Run all quality checks - -# Local CI Testing with act -########################### - -# Detect docker socket (Colima, Docker, or Podman) -define docker_socket -$(shell \ - if [ -S $$HOME/.colima/default/docker.sock ]; then \ - echo "$$HOME/.colima/default/docker.sock"; \ - elif [ -S /var/run/docker.sock ]; then \ - echo "/var/run/docker.sock"; \ - elif [ -S $$HOME/.docker/run/docker.sock ]; then \ - echo "$$HOME/.docker/run/docker.sock"; \ - elif [ -S /run/user/$$(id -u)/podman/podman.sock ]; then \ - echo "/run/user/$$(id -u)/podman/podman.sock"; \ - else \ - echo "/var/run/docker.sock"; \ - fi \ -) -endef - -DOCKER_SOCKET := $(docker_socket) -ACT_IMAGE := catthehacker/ubuntu:act-22.04 -ACT_ARCH := linux/amd64 - -act-check: # Check if act is installed, install if missing - @if ! which act > /dev/null 2>&1; then \ - echo "⚠️ act is not installed. Installing automatically..."; \ - $(MAKE) act-install; \ - fi - -docker-check: # Check if Docker/Podman/Colima is running - @if ! DOCKER_HOST="unix://$(DOCKER_SOCKET)" docker ps >/dev/null 2>&1; then \ - echo "❌ Cannot connect to Docker daemon"; \ - echo ""; \ - echo "Please start your container runtime first:"; \ - echo " β€’ Docker Desktop: Open Docker Desktop app"; \ - echo " β€’ Colima: run 'colima start'"; \ - echo " β€’ Podman: run 'podman machine start'"; \ - echo " β€’ Docker (Linux): run 'sudo systemctl start docker'"; \ - echo ""; \ - echo "Attempted socket: $(DOCKER_SOCKET)"; \ - exit 1; \ - fi - @echo "βœ“ Docker is running (socket: $(DOCKER_SOCKET))" - -act-install: ## Install act for local CI testing - @echo "Installing act (GitHub Actions local runner)..." - @if which act > /dev/null 2>&1; then \ - echo "βœ… act is already installed: $$(which act)"; \ - act --version; \ - elif which brew > /dev/null 2>&1; then \ - echo "πŸ“¦ Installing act via Homebrew..."; \ - brew install act; \ - else \ - echo "πŸ“¦ Installing act via install script..."; \ - curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash; \ - fi - @echo "" - @echo "βœ… act installed successfully!" - @echo "πŸ’‘ Run 'make ci-list' to see available workflows" - @echo "πŸ’‘ Run 'make ci-local' to run CI checks locally" - -ci-list: act-check docker-check ## List available GitHub Actions workflows and jobs - @echo "πŸ“‹ Available workflows and jobs:" - @DOCKER_HOST="unix://$(DOCKER_SOCKET)" act -l - -ci-local: act-check docker-check ## Run CI checks locally (tests workflow) - @echo "πŸ”¬ Running local CI checks..." - @echo "Using Docker socket: $(DOCKER_SOCKET)" - @echo "Container architecture: $(ACT_ARCH)" - @echo "Image: $(ACT_IMAGE)" - @echo "" - @JOB=$${JOB:-checks}; \ - DOCKER_HOST="unix://$(DOCKER_SOCKET)" act pull_request \ - -W .github/workflows/tests.yml \ - -j $$JOB \ - --container-daemon-socket - \ - --container-architecture $(ACT_ARCH) \ - -P ubuntu-latest=$(ACT_IMAGE) - @echo "" - @echo "βœ… Local CI checks complete!" - -ci-local-docs: act-check docker-check ## Run documentation build check locally - @echo "πŸ“š Running local documentation build check..." - @DOCKER_HOST="unix://$(DOCKER_SOCKET)" act pull_request \ - -W .github/workflows/docs.yml \ - -j build-check \ - --container-daemon-socket - \ - --container-architecture $(ACT_ARCH) \ - -P ubuntu-latest=$(ACT_IMAGE) - @echo "" - @echo "βœ… Documentation build check complete!" - -ci-debug: act-check docker-check ## Fast local test debugging (uses ci-debug.yml workflow) - @echo "πŸ”§ Running CI debug workflow for fast iteration" - @echo "πŸ’‘ Edit .github/workflows/ci-debug.yml to customize which tests run" - @echo "" - @DOCKER_HOST="unix://$(DOCKER_SOCKET)" act workflow_dispatch \ - -W .github/workflows/ci-debug.yml \ - --container-daemon-socket - \ - --container-architecture $(ACT_ARCH) \ - -P ubuntu-latest=$(ACT_IMAGE) - @echo "" - @echo "βœ… Debug workflow complete!" - -ci-clean: ## Clean up act cache and containers - @echo "🧹 Cleaning up act cache and containers..." - @-docker ps -a | grep "act-" | awk '{print $$1}' | xargs docker rm -f 2>/dev/null || true - @-docker images | grep "act-" | awk '{print $$3}' | xargs docker rmi -f 2>/dev/null || true - @echo "βœ… Cleanup complete!" - -# Documentation -############### -DOCS_PORT ?= 8000 - -docs-install: setup ## Install documentation dependencies - @echo "Installing documentation dependencies..." - uv sync --group dev - @echo "Documentation dependencies installed" - -docs-build: docs-install ## Build documentation site - @echo "Building documentation..." - uv run mkdocs build --strict - @echo "Documentation built successfully" - @echo "πŸ“„ Site location: site/" - @echo "🌐 Open site/index.html in your browser to view" - -docs-serve: docs-install ## Serve documentation locally with live reload - @echo "Starting documentation server with live reload..." - @echo "πŸ“ Documentation will be available at:" - @echo " - Local: http://localhost:$(DOCS_PORT)" - @echo "πŸ”„ Changes will auto-reload (press Ctrl+C to stop)" - @echo "" - @echo "πŸ’‘ To use a different port: make docs-serve DOCS_PORT=9999" - uv run mkdocs serve --dev-addr 0.0.0.0:$(DOCS_PORT) - -docs-check: docs-build ## Check documentation build and links - @echo "Checking documentation..." - @echo "πŸ“Š Site size: $$(du -sh site/ | cut -f1)" - @echo "πŸ“„ Pages built: $$(find site/ -name "*.html" | wc -l)" - @echo "πŸ”— Checking for common issues..." - @if grep -r "404" site/ >/dev/null 2>&1; then \ - echo "⚠️ Found potential 404 errors"; \ - else \ - echo "βœ… No obvious 404 errors found"; \ - fi - @if find site/ -name "*.html" -size 0 | grep -q .; then \ - echo "⚠️ Found empty HTML files"; \ - find site/ -name "*.html" -size 0; \ - else \ - echo "βœ… No empty HTML files found"; \ - fi - @echo "Documentation check complete" - -docs-clean: ## Clean documentation build files - @echo "Cleaning documentation build files..." - rm -rf site/ - rm -rf .cache/ - @echo "Documentation cleaned" - -# Project Management -################## -clean-example: # Remove example code (use this to start your own project) - rm -rf $(MODULE_NAME)/example.py tests/test_example.py - touch $(MODULE_NAME)/__init__.py tests/__init__.py - -init: setup # Initialize a new project - uv run python scripts/init_project.py - -# Container Engine Support -######################## -# Auto-detect container engine (podman or docker) -CONTAINER_ENGINE ?= $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker) - -# Podman-specific adjustments -ifeq ($(CONTAINER_ENGINE),podman) - # Use podman-compose for compose functionality - COMPOSE_CMD = podman-compose - # Use host UID/GID for rootless containers - CONTAINER_USER_OPTS = --userns=keep-id - # Podman machine status check - PODMAN_MACHINE_RUNNING = $(shell podman machine list --format json 2>/dev/null | grep '"Running": true' >/dev/null && echo yes || echo no) -else - # Docker: use native compose - COMPOSE_CMD = $(CONTAINER_ENGINE) compose - # Docker: use current user's UID/GID to avoid permission issues - CONTAINER_USER_OPTS = --user $(shell id -u):$(shell id -g) -endif - -# Docker/Podman Images -##################### -IMAGE_NAME = container-registry.io/python-collab-template -IMAGE_TAG = latest - -dev-env: ensure-container-ready refresh-containers - @echo "Spinning up a dev environment using $(CONTAINER_ENGINE)..." - @$(COMPOSE_CMD) -f docker/docker-compose.yml down - @$(COMPOSE_CMD) -f docker/docker-compose.yml up -d dev - @$(CONTAINER_ENGINE) exec -ti composed_dev /bin/bash - -refresh-containers: ensure-container-ready - @echo "Rebuilding containers using $(CONTAINER_ENGINE)..." - @$(COMPOSE_CMD) -f docker/docker-compose.yml build - -rebuild-images: - @echo "Rebuilding images with the --no-cache flag using $(CONTAINER_ENGINE)..." - @$(COMPOSE_CMD) -f docker/docker-compose.yml build --no-cache - -build-image: - @echo Building dev image using $(CONTAINER_ENGINE) and tagging as ${IMAGE_NAME}:${IMAGE_TAG} - @$(COMPOSE_CMD) -f docker/docker-compose.yml down - @$(COMPOSE_CMD) -f docker/docker-compose.yml up -d dev - @$(CONTAINER_ENGINE) tag dev ${IMAGE_NAME}:${IMAGE_TAG} - -push-image: build-image - @echo Pushing image to container registry using $(CONTAINER_ENGINE) - @$(CONTAINER_ENGINE) push ${IMAGE_NAME}:${IMAGE_TAG} - -# Container Engine Info -###################### -ensure-container-ready: # Ensure container engine is ready -ifeq ($(CONTAINER_ENGINE),podman) - @echo "Checking Podman machine status..." - @if [ "$(PODMAN_MACHINE_RUNNING)" = "no" ]; then \ - echo "Podman machine is not running. Starting it..."; \ - podman machine start; \ - echo "Waiting for Podman machine to be ready..."; \ - sleep 3; \ - fi - @if ! command -v podman-compose >/dev/null 2>&1; then \ - echo "Error: podman-compose not found. Install with: brew install podman-compose"; \ - exit 1; \ - fi -else - @echo "Using Docker engine..." -endif - -container-info: # Display detected container engine and configuration - @echo "Container Engine: $(CONTAINER_ENGINE)" - @echo "Compose Command: $(COMPOSE_CMD)" - @echo "User Options: $(CONTAINER_USER_OPTS)" -ifeq ($(CONTAINER_ENGINE),podman) - @echo "Podman Machine Running: $(PODMAN_MACHINE_RUNNING)" - @echo "podman-compose Available: $(shell command -v podman-compose >/dev/null 2>&1 && echo yes || echo no)" -endif - @echo "" - @echo "To override, use: CONTAINER_ENGINE=podman make dev-env" diff --git a/README.md b/README.md index f8a62a7..361d1c5 100644 --- a/README.md +++ b/README.md @@ -1,198 +1,101 @@ -![Code Quality Checks](https://github.com/safurrier/python-collab-template/workflows/Code%20Quality%20Checks/badge.svg) [![codecov](https://codecov.io/gh/safurrier/python-collab-template/branch/master/graph/badge.svg)](https://codecov.io/gh/safurrier/python-collab-template) +# Claude PR BugScan -# Python Project Template +Reusable GitHub Actions workflow that runs [Claude Code Action](https://github.com/anthropics/claude-code-action) to review PR diffs using the official Codex Code Review prompt. -A modern Python project template with best practices for development and collaboration. +## What this does -## Features -- πŸš€ Fast dependency management with [uv](https://github.com/astral-sh/uv) -- ✨ Code formatting with [ruff](https://github.com/astral-sh/ruff) -- πŸ” Type checking with [ty](https://astral.sh/blog/ty) -- πŸ§ͺ Testing with [pytest](https://github.com/pytest-dev/pytest) -- 🐳 Docker support for development and deployment -- πŸ‘· CI/CD with GitHub Actions +- Automatically reviews PRs when they transition from **Draft β†’ Ready for review** +- Re-runs on **new commits pushed** to an open PR +- Skips draft PRs entirely +- Posts review output back to the PR as comments +- Uses the [official Codex Code Review prompt](https://developers.openai.com/cookbook/examples/codex/build_code_review_with_codex_sdk/) verbatim -## Python Version -This template requires Python 3.9 or higher and defaults to Python 3.12. To use a different version: +## Repository layout -```bash -# List available Python versions -uv python list - -# Use a specific version (e.g., 3.11) -make setup PYTHON_VERSION=3.11 # or UV_PYTHON_VERSION=3.11 make setup - -# View installed Python versions -uv python list --installed -``` - -uv will automatically download and manage Python versions as needed. - -## Quickstart -```bash -# Clone this repo and change directory -git clone git@github.com:safurrier/python-collab-template.git my-project-name -cd my-project-name - -# Initialize a new project -make init - -# Follow the prompts to configure your project -``` - -This will: -- Configure project metadata (name, description, author) -- Handle example code (keep, simplify, or remove) -- Initialize a fresh git repository -- Set up development environment -- Configure pre-commit hooks (optional, enabled by default) - -Pre-commit hooks will automatically run these checks before each commit: -- Type checking (ty) -- Linting (ruff) -- Formatting (ruff) -- Tests (pytest) - -Alternatively, you can set up manually: -```bash -# Install dependencies and set up the environment -make setup - -# Run the suite of tests and checks -make check - -# Optional: Remove example code to start fresh -make clean-example ``` - -## Development Commands - -### Quality Checks -```bash -make check # Run all checks (test, ty, lint, format) -make test # Run tests with coverage -make ty # Run type checking -make lint # Run linter -make format # Run code formatter +.github/workflows/ + claude_pr_review.yml # Reusable workflow (workflow_call) + ai_pr_review.yml # Caller workflow for this repo (example) +prompts/ + codex_code_review_prompt.md # Official Codex prompt text (verbatim) ``` -### Local CI Testing - -Run GitHub Actions workflows locally before pushing using [act](https://github.com/nektos/act): +## Using this in another repo -```bash -# Run full test suite locally (auto-installs act if needed) -make ci-local +### 1. Add the caller workflow -# List available workflows -make ci-list +Create `.github/workflows/ai_pr_review.yml` in your target repo: -# Run specific job -JOB=checks make ci-local +```yaml +name: Claude PR scan (Draft->Ready + Push) -# Run documentation build check -make ci-local-docs +on: + pull_request: + types: [ready_for_review, synchronize, reopened] -# Fast debugging (customize .github/workflows/ci-debug.yml) -make ci-debug +concurrency: + group: claude-pr-scan-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true -# Clean up act containers -make ci-clean +jobs: + scan: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` -**Note:** The first run will automatically install `act` if it's not present. +### 2. Add the secret -**Benefits:** -- 5-20 second feedback vs. 2-5 minutes on GitHub -- Test before commit/push -- No GitHub Actions minutes consumed -- Debug workflows locally +In your target repo: **Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret** -**Troubleshooting:** +- Name: `ANTHROPIC_API_KEY` +- Value: your Anthropic API key -*Linux: Docker permissions* -```bash -# Add your user to the docker group -sudo usermod -aG docker $USER +### 3. Pin to a release tag -# Log out and back in for changes to take effect -# Or run: newgrp docker +Reference `@v1` (or a specific tag) to pin to a stable version. Check the [releases](../../releases) page for available tags. -# Verify it works -docker ps -``` - -*macOS: Colima disk lock errors* -```bash -# If you get "disk in use" or similar errors: -colima stop -colima delete -colima start -``` +## Customizing the prompt -*General: Stale act containers* -```bash -# Clean up old containers and images -make ci-clean -``` - -### Example Code -The repository includes a simple example showing: -- Type hints -- Dataclasses -- Unit tests -- Modern Python practices +To override the default Codex prompt for a specific repo, pass `prompt_override`: -To remove the example code and start fresh: -```bash -make clean-example +```yaml +jobs: + scan: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + prompt_override: | + ``` -## Container Support (Docker/Podman) - -### Development Environment -The project automatically detects and uses either Docker or Podman: +## Trigger policy -```bash -make dev-env # Uses podman if available, otherwise docker +| Event | Runs? | +|---|---| +| PR opened as draft | No (skipped by `if` guard) | +| Draft PR gets new commits | No (skipped by `if` guard) | +| PR marked Ready for review | Yes (`ready_for_review`) | +| New commits pushed to open PR | Yes (`synchronize`) | +| Closed PR reopened | Yes (`reopened`), skipped if still draft | -# Or explicitly choose: -CONTAINER_ENGINE=docker make dev-env -CONTAINER_ENGINE=podman make dev-env +Concurrency is keyed on `github.repository + PR number` so rapid pushes cancel the in-progress scan and start fresh. -# Check which engine will be used: -make container-info -``` - -This creates a container with: -- All dependencies installed -- Source code mounted (changes reflect immediately) -- Development tools ready to use -- Automatic UID/GID mapping for file permissions +## Versioning -### Production Image -```bash -make build-image # Build production image -make push-image # Push to container registry -``` +- `v1` β€” initial release +- Tag releases with `git tag v1 && git push origin v1` -## Project Structure -``` -. -β”œβ”€β”€ src/ # Source code -β”œβ”€β”€ tests/ # Test files -β”œβ”€β”€ docker/ # Container configuration (Docker/Podman) -β”œβ”€β”€ .github/ # GitHub Actions workflows -β”œβ”€β”€ pyproject.toml # Project configuration -└── Makefile # Development commands -``` +## Permissions -## Contributing -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Run `make check` to ensure all tests pass -5. Submit a pull request +The reusable workflow requests only: +- `contents: read` +- `pull-requests: write` +- `issues: write` ## License -This project is licensed under the MIT License - see the LICENSE file for details. + +MIT diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 0cdf627..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM python:3.12-slim - -# Install Auxiliary Software -RUN apt-get update && apt-get install -y \ - make \ - apt-utils \ - apt-transport-https \ - curl \ - gcc \ - gnupg \ - gnupg-agent \ - graphviz \ - software-properties-common \ - vim \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /workspace - -# Copy essential files for dependency resolution to temp location -COPY pyproject.toml uv.lock /tmp/ -COPY README.md /tmp/ - -RUN pip install -U pip uv \ - && cd /tmp \ - && uv sync --system --all-extras \ - && rm -rf /tmp/pyproject.toml /tmp/uv.lock /tmp/README.md - -CMD ["/bin/bash"] diff --git a/docker/Makefile b/docker/Makefile deleted file mode 100644 index 190d502..0000000 --- a/docker/Makefile +++ /dev/null @@ -1,61 +0,0 @@ -setup: - pip install --upgrade pip - pip install -r requirements.dev - pip install -r requirements.prod - -clean-pyc: - find . -name '*.pyc' -exec rm -f {} + - find . -name '*.pyo' -exec rm -f {} + - find . -name '*~' -exec rm -f {} + - find . -name '__pycache__' -exec rm -fr {} + - -clean-test: - rm -f .coverage - rm -f .coverage.* - -clean: clean-pyc clean-test - -test: clean - py.test tests --cov=src --cov-report=term-missing --cov-fail-under 95 - -ty: - ty check src - -lint: - pylint src -j 4 --reports=y - -docs: FORCE - cd docs; sphinx-apidoc -o ./source ./src - cd docs; sphinx-build -b html ./source ./build -FORCE: - -check: test lint ty - -# Docker -######## -IMAGE_NAME = container-registry.io/python-collab-template -IMAGE_TAG = latest - -dev-env: refresh-containers - @echo "Spinning up a dev environment ." - @docker compose -f docker/docker-compose.yml down - @docker compose -f docker/docker-compose.yml up -d dev - @docker exec -ti composed_dev /bin/bash - -refresh-containers: - @echo "Rebuilding containers..." - @docker compose -f docker/docker-compose.yml build - -rebuild-images: - @echo "Rebuilding images with the --no-cache flag..." - @docker compose -f docker/docker-compose.yml build --no-cache - -build-image: - @echo Building dev image and tagging as ${IMAGE_NAME}:${IMAGE_TAG} - @docker compose -f docker/docker-compose.yml down - @docker compose -f docker/docker-compose.yml up -d dev - @docker tag dev ${IMAGE_NAME}:${IMAGE_TAG} - -push-image: build-image - @echo Pushing image to container registry - @docker push ${IMAGE_NAME}:${IMAGE_TAG} \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index eed0854..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: "3.7" -services: - dev: - tty: true # Keep Alive - image: dev - container_name: composed_dev - build: - context: .. - dockerfile: docker/Dockerfile - environment: - EXAMPLE: ${EXAMPLE} - EXAMPLE_SECRET: ${EXAMPLE_SECRET} - command: /bin/bash - # Note: user mapping handled by Podman rootless automatically - # Bind whole project directory for simplicity - volumes: - - ../:/workspace:Z diff --git a/docker/template.env b/docker/template.env deleted file mode 100644 index 1b7ed75..0000000 --- a/docker/template.env +++ /dev/null @@ -1,15 +0,0 @@ -# Development environment variables -PYTHON_ENV=development - -# Example variables (replace with your own) -APP_NAME=python-collab-template -DEBUG=true - -# Secrets (do not commit actual values) -API_KEY= -DATABASE_URL= - -# Container runtime settings (optional) -# UID=${UID} # Uncomment to use your system UID -# GID=${GID} # Uncomment to use your system GID -# DOCKER_SOCK=/var/run/docker.sock # Override socket path for Podman diff --git a/docs/container-setup.md b/docs/container-setup.md deleted file mode 100644 index 58d4f72..0000000 --- a/docs/container-setup.md +++ /dev/null @@ -1,138 +0,0 @@ -# Container Setup Guide - -This project supports both Docker and Podman for containerized development. - -## Quick Start - -The Makefile automatically detects your container engine: - -```bash -# Automatic detection (prefers Podman if available) -make dev-env - -# Explicit engine selection -CONTAINER_ENGINE=docker make dev-env -CONTAINER_ENGINE=podman make dev-env -``` - -## Podman vs Docker - -### Key Differences - -| Feature | Docker | Podman | -|---------|--------|--------| -| Root privileges | Runs as root by default | Rootless by default | -| Daemon | Requires dockerd daemon | Daemonless | -| Security | Good with proper setup | Better default security | -| Compose support | Native | Via podman-compose | - -### When to Use Which - -**Use Docker when:** -- It's your team's standard -- You need Docker Desktop features -- You're using Docker-specific tooling - -**Use Podman when:** -- Security is a top priority -- You can't/don't want to run a daemon -- You're in a restricted environment - -## Troubleshooting - -### Permission Issues - -If you encounter permission issues with mounted volumes: - -1. **For Podman**: Should work automatically with rootless mode -2. **For Docker**: Set your UID/GID in `docker/.env`: - ```bash - echo "UID=$(id -u)" >> docker/.env - echo "GID=$(id -g)" >> docker/.env - ``` - -### Socket Issues - -If Podman can't find the Docker socket: - -```bash -# Set the socket path in your .env -echo "DOCKER_SOCK=${XDG_RUNTIME_DIR}/podman/podman.sock" >> docker/.env -``` - -### Compose Command Not Found - -For Podman, you need to install podman-compose: - -```bash -# macOS -brew install podman-compose - -# Linux -pip install podman-compose -``` - -### Podman Machine Not Running (macOS/Windows) - -Podman needs a Linux VM to run containers. The Makefile will automatically start it, but you can also manage it manually: - -```bash -# Initialize a new machine -podman machine init - -# Start the machine -podman machine start - -# Check machine status -podman machine list - -# Stop the machine -podman machine stop -``` - -### Auto-Setup with Make - -The project's Makefile handles most Podman setup automatically: - -- Checks if Podman machine is running -- Starts it if needed -- Verifies podman-compose is installed -- Uses appropriate socket paths - -Just run `make container-info` to see the current status. - -## Compatibility with Project Initialization - -The container setup is designed to work both before and after running `make init`: - -### Before `make init` -- Source code is in `src/` directory -- Container mounts entire project as `/workspace` -- All development tools work normally - -### After `make init` -- Source code moves to your project module directory (e.g., `my_project/`) -- Container setup continues to work unchanged -- Volume mounts and dependencies remain intact - -The `make init` command: -1. Renames `src/` to your project name -2. Updates import statements in tests -3. Modifies `pyproject.toml` and `Makefile` - -**The Docker/Podman setup survives this transformation** because: -- The Dockerfile doesn't hardcode directory names -- Dependencies are installed from temporary copied files -- The entire project is mounted, regardless of internal structure - -This means you can: -```bash -# Set up development environment -make dev-env - -# Initialize your project later -make init - -# Continue using the same development environment -make dev-env # Still works! -``` \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 6ea4e4a..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,143 +0,0 @@ -# Getting Started with Python Collab Template - -This guide walks you through using this template to create a new Python project. - -## Prerequisites - -- Python 3.9 or higher -- Git -- GitHub account (for template usage) - -## Creating a Project from This Template - -### Step 1: Create Repository from Template - -1. Go to this template repository on GitHub -2. Click **"Use this template"** button -3. Choose **"Create a new repository"** -4. Fill in your repository details: - - Repository name (e.g., `my-awesome-project`) - - Description - - Public/Private visibility - -### Step 2: Clone and Initialize - -1. Clone your new repository: - ```bash - git clone https://github.com/your-username/your-project-name.git - cd your-project-name - ``` - -2. Initialize your project: - ```bash - make init - ``` - -3. Follow the interactive prompts: - - **Project name**: Enter your project name (e.g., "My Awesome Project") - - **Description**: Brief description of your project - - **Author info**: Your name and email (auto-detected from git config) - - **Example code**: Choose how to handle example code: - - Keep (useful for reference) - - Minimal (basic working example) - - Remove (clean slate) - - **Documentation**: Set up MkDocs documentation (default: yes) - - **Pre-commit hooks**: Enable quality checks on commit (default: yes) - -### Step 3: Verify Setup - -After initialization, verify everything works: - -```bash -# Run all quality checks -make check - -# If you enabled documentation -make docs-serve -``` - -## Project Structure After Initialization - -Your initialized project will have: - -``` -your-project-name/ -β”œβ”€β”€ your_project_name/ # Main package (renamed from src/) -β”œβ”€β”€ tests/ # Test files -β”œβ”€β”€ docs/ # Documentation (if enabled) -β”œβ”€β”€ .github/workflows/ # CI/CD workflows -β”œβ”€β”€ pyproject.toml # Project configuration -β”œβ”€β”€ Makefile # Development commands -└── README.md # Project documentation -``` - -## Development Workflow - -### Daily Development - -1. Make your changes to the code -2. Add or update tests -3. Run quality checks: - ```bash - make check - ``` -4. Update documentation if needed -5. Commit and push - -### Available Commands - -Your project comes with these make targets: - -- `make setup` - Set up development environment -- `make test` - Run tests with coverage -- `make lint` - Run linting with auto-fix -- `make format` - Format code -- `make ty` - Run type checking -- `make check` - Run all quality checks -- `make docs-serve` - Serve documentation locally (if enabled) -- `make docs-build` - Build documentation (if enabled) -- `make docs-check` - Validate documentation build (if enabled) - -### Documentation (If Enabled) - -If you chose to set up documentation: - -1. **Local development**: - ```bash - make docs-serve - # Visit http://localhost:8000 - ``` - -2. **Content organization**: - - `docs/index.md` - Project homepage - - `docs/getting-started.md` - User guide - - `docs/reference/api.md` - Auto-generated API docs - -3. **GitHub Pages setup**: - - Go to repository Settings β†’ Pages - - Set source to "GitHub Actions" - - Documentation will auto-deploy on main branch pushes - -### CI/CD - -Your project includes GitHub Actions workflows: - -- **Quality checks** (`tests.yml`): Runs on PRs and pushes -- **Documentation** (`docs.yml`): Deploys docs to GitHub Pages (if enabled) - -## Next Steps - -1. **Update README.md** with project-specific information -2. **Add your code** in the main package directory -3. **Write tests** for your functionality -4. **Update documentation** to describe your project -5. **Set up GitHub Pages** (if documentation enabled) -6. **Configure repository settings** (branch protection, etc.) - -## Tips - -- The template includes example code you can reference or remove -- All configuration follows modern Python best practices -- The documentation system auto-generates API docs from docstrings -- Pre-commit hooks ensure code quality before commits -- Use `make` commands for consistent development workflow \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index c9fa871..0000000 --- a/docs/index.md +++ /dev/null @@ -1,67 +0,0 @@ -# Python Collab Template - -A modern, collaborative Python project template with comprehensive tooling and best practices built-in. - -## 🎯 Template Features - -This template provides everything you need for a professional Python project: - -- πŸ”§ **Modern Tooling**: UV package manager, Ruff formatting/linting, MyPy type checking -- πŸ§ͺ **Testing**: pytest with coverage reporting and CI integration -- πŸ“š **Documentation**: Optional MkDocs + Material theme with auto-generation -- πŸš€ **CI/CD**: GitHub Actions with quality checks and automated deployment -- 🐳 **Development**: Docker support and pre-commit hooks -- πŸ“¦ **Packaging**: Modern pyproject.toml configuration with hatchling - -## πŸš€ Quick Start - -### Using This Template - -1. **Create a new repository** from this template on GitHub -2. **Clone your new repository**: - ```bash - git clone https://github.com/your-username/your-project-name.git - cd your-project-name - ``` -3. **Initialize your project**: - ```bash - make init - ``` -4. **Follow the prompts** to customize your project - -### What `make init` Does - -The initialization script will: -- Prompt for project name, description, and author information -- Update all configuration files with your project details -- Choose how to handle example code (keep, simplify, or remove) -- Optionally set up MkDocs documentation (default: yes) -- Rename directories and update imports -- Set up git repository and pre-commit hooks - -## πŸ“ Template Structure - -``` -python-collab-template/ -β”œβ”€β”€ src/ # Source code (renamed during init) -β”œβ”€β”€ tests/ # Test files -β”œβ”€β”€ scripts/ # Utility scripts (including init) -β”œβ”€β”€ templates/ # Documentation templates -β”œβ”€β”€ docker/ # Docker configuration -β”œβ”€β”€ .github/workflows/ # CI/CD automation -β”œβ”€β”€ pyproject.toml # Project configuration -β”œβ”€β”€ Makefile # Development commands -└── README.md # Project documentation -``` - -## πŸ› οΈ Available Commands - -After initialization, your project will have these commands: - -- `make setup` - Set up development environment -- `make test` - Run tests with coverage -- `make check` - Run all quality checks -- `make docs-serve` - Serve documentation locally (if enabled) -- `make docs-build` - Build documentation (if enabled) - -For complete usage instructions, see the [Getting Started](getting-started.md) guide. \ No newline at end of file diff --git a/docs/reference/api.md b/docs/reference/api.md deleted file mode 100644 index 08b135d..0000000 --- a/docs/reference/api.md +++ /dev/null @@ -1,9 +0,0 @@ -# API Reference - -This page contains the auto-generated API documentation for the project. - -::: src - options: - show_root_heading: true - members_order: source - show_source: false \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index e5582e0..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,80 +0,0 @@ -site_name: Python Collab Template -site_url: https://safurrier.github.io/python-collab-template/ -site_description: A collaborative Python project template with modern tooling -site_author: safurrier - -repo_name: safurrier/python-collab-template -repo_url: https://github.com/safurrier/python-collab-template -edit_uri: edit/main/docs/ - -theme: - name: material - features: - - navigation.tabs - - navigation.tabs.sticky - - navigation.sections - - navigation.top - - navigation.tracking - - search.suggest - - search.highlight - - search.share - - toc.follow - - content.action.edit - - content.code.copy - - content.code.annotate - palette: - - scheme: default - primary: indigo - accent: indigo - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - scheme: slate - primary: indigo - accent: indigo - toggle: - icon: material/brightness-4 - name: Switch to light mode - font: - text: Roboto - code: Roboto Mono - icon: - logo: material/library - -plugins: - - search - - mkdocstrings: - handlers: - python: - paths: [src] - options: - show_source: true - show_root_heading: true - merge_init_into_class: true - docstring_style: google - show_signature_annotations: true - separate_signature: true - -markdown_extensions: - - admonition - - attr_list - - pymdownx.details - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - toc: - permalink: true - -nav: - - Home: index.md - - Getting Started: getting-started.md - - Reference: - - API Documentation: reference/api.md - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/safurrier/python-collab-template \ No newline at end of file diff --git a/prompts/codex_code_review_prompt.md b/prompts/codex_code_review_prompt.md new file mode 100644 index 0000000..f5a1969 --- /dev/null +++ b/prompts/codex_code_review_prompt.md @@ -0,0 +1 @@ +You are acting as a reviewer for a proposed code change made by another engineer. Focus on issues that impact correctness, performance, security, maintainability, or developer experience. Flag only actionable issues introduced by the pull request. When you flag an issue, provide a short, direct explanation and cite the affected file and line range. Prioritize severe issues and avoid nit-level comments unless they block understanding of the diff. After listing findings, produce an overall correctness verdict ("patch is correct" or "patch is incorrect") with a concise justification and a confidence score between 0 and 1. Ensure that file citations and line numbers are exactly correct using the tools available; if they are incorrect your comments will be rejected. diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 445c5a9..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,78 +0,0 @@ -[project] -name = "python-collab-template" -version = "0.1.0" -description = "Add your description here" -authors = [ - { name = "alex furrier", email = "afurrier@gmail.com" } -] -requires-python = ">= 3.9" -readme = "README.md" -dependencies = [] # No runtime dependencies needed for example - -[project.optional-dependencies] -dev = [ - "ty>=0.0.2", # Type checking - "pytest>=8.1.1", # Testing - "pytest-cov>=5.0.0", # Coverage reporting - "ruff>=0.3.0", # Linting and formatting - "pre-commit>=3.6.0", # Pre-commit hooks - "tomli>=2.0.1", # TOML reading - "tomli-w>=1.0.0", # TOML writing -] - -[build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[dependency-groups] -dev = [ - "mkdocs-material>=9.6.14", - "mkdocstrings[python]>=0.26.1", -] - -[tool.hatch.build.targets.wheel] -packages = ["src"] - -[tool.ruff] -line-length = 88 -target-version = "py39" - -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade - "TID", # flake8-tidy-imports - "E402", # module-import-not-at-top-of-file - "N", # pep8-naming - "S", # flake8-bandit - "PTH", # flake8-use-pathlib - "RUF", # ruff-specific rules - "ICN", # flake8-import-conventions -] - -ignore = [ - "E501", # line too long (handled by line-length) - "TID252", # prefer absolute imports (keep relative for internal imports) - "S101", # assert used (acceptable in tests) -] - -[tool.ruff.lint.flake8-tidy-imports] -# Prevent specific problematic imports -banned-api = {} - -[tool.ruff.lint.isort] -known-first-party = ["python-collab-template"] - -[tool.ty.environment] -python-version = "3.9" - -[tool.ty.src] -include = ["src"] - -[tool.ty.terminal] -error-on-warning = true diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 6860cba..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,46 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml --extra dev -o requirements-dev.txt -cfgv==3.4.0 - # via pre-commit -coverage==7.9.1 - # via pytest-cov -distlib==0.3.9 - # via virtualenv -filelock==3.18.0 - # via virtualenv -identify==2.6.12 - # via pre-commit -iniconfig==2.1.0 - # via pytest -nodeenv==1.9.1 - # via pre-commit -packaging==25.0 - # via pytest -platformdirs==4.3.8 - # via virtualenv -pluggy==1.6.0 - # via - # pytest - # pytest-cov -pre-commit==4.2.0 - # via python-collab-template (pyproject.toml) -pygments==2.19.1 - # via pytest -pytest==8.4.1 - # via - # python-collab-template (pyproject.toml) - # pytest-cov -pytest-cov==6.2.1 - # via python-collab-template (pyproject.toml) -pyyaml==6.0.2 - # via pre-commit -ruff==0.12.0 - # via python-collab-template (pyproject.toml) -tomli==2.2.1 - # via python-collab-template (pyproject.toml) -tomli-w==1.2.0 - # via python-collab-template (pyproject.toml) -ty==0.0.2 - # via python-collab-template (pyproject.toml) -virtualenv==20.31.2 - # via pre-commit diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9a56360..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -# This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml -o requirements.txt diff --git a/scripts/init_project.py b/scripts/init_project.py deleted file mode 100755 index cbde75d..0000000 --- a/scripts/init_project.py +++ /dev/null @@ -1,389 +0,0 @@ -import os -import subprocess -import sys -from pathlib import Path -from typing import Optional -import tomli -import tomli_w - - -def prompt_with_default(prompt: str, default: str) -> str: - """Prompt for input with a default value.""" - response = input(f"{prompt} [{default}]: ").strip() - return response if response else default - - -def get_git_config(key: str) -> Optional[str]: - """Get git config value.""" - try: - return subprocess.check_output( - ["git", "config", "user."+key], text=True - ).strip() - except subprocess.CalledProcessError: - return None - - -def update_pyproject_toml( - project_name: str, - project_description: str, - author_name: str, - author_email: str -) -> None: - """Update pyproject.toml with new project information.""" - pyproject_path = Path("pyproject.toml") - - # Read existing toml - with open(pyproject_path, "rb") as f: - config = tomli.load(f) - - # Update project information - config["project"]["name"] = project_name - config["project"]["description"] = project_description - config["project"]["authors"] = [ - {"name": author_name, "email": author_email} - ] - - # Write updated toml - with open(pyproject_path, "wb") as f: - tomli_w.dump(config, f) - - -def run_command(command: str) -> None: - """Run a shell command and exit if it fails.""" - try: - subprocess.run(command, shell=True, check=True) - except subprocess.CalledProcessError as e: - print(f"Error running command: {command}") - print(f"Error: {e}") - sys.exit(1) - - -def template_file(template_path: str, output_path: str, replacements: dict) -> None: - """Template a file by replacing placeholders with values.""" - with open(template_path, "r") as f: - content = f.read() - - for placeholder, value in replacements.items(): - content = content.replace(f"{{{placeholder}}}", value) - - # Ensure parent directory exists (only if there is a parent directory) - parent_dir = os.path.dirname(output_path) - if parent_dir: - os.makedirs(parent_dir, exist_ok=True) - - with open(output_path, "w") as f: - f.write(content) - - -def setup_documentation( - project_name: str, - project_description: str, - author_name: str, - author_email: str, - project_module_name: str, - github_username: str = "your-username" -) -> None: - """Set up documentation with MkDocs + Material theme.""" - print("πŸ“š Setting up documentation...") - - # Define template replacements - replacements = { - "project_name": project_name, - "project_description": project_description, - "author_name": author_name, - "author_email": author_email, - "project_module_name": project_module_name, - "github_username": github_username, - } - - # Template and create documentation files - template_file("templates/mkdocs.yml.template", "mkdocs.yml", replacements) - template_file("templates/docs/index.md.template", "docs/index.md", replacements) - template_file("templates/docs/getting-started.md.template", "docs/getting-started.md", replacements) - template_file("templates/docs/reference/api.md.template", "docs/reference/api.md", replacements) - template_file("templates/.github/workflows/docs.yml.template", ".github/workflows/docs.yml", replacements) - - # Add documentation dependencies to pyproject.toml - pyproject_path = Path("pyproject.toml") - with open(pyproject_path, "rb") as f: - config = tomli.load(f) - - # Ensure dependency-groups exists - if "dependency-groups" not in config: - config["dependency-groups"] = {} - - # Add docs dependencies to dev group - if "dev" not in config["dependency-groups"]: - config["dependency-groups"]["dev"] = [] - - docs_deps = [ - "mkdocs-material>=9.6.14", - "mkdocstrings[python]>=0.26.1", - ] - - for dep in docs_deps: - if dep not in config["dependency-groups"]["dev"]: - config["dependency-groups"]["dev"].append(dep) - - with open(pyproject_path, "wb") as f: - tomli_w.dump(config, f) - - print("βœ… Documentation setup complete") - print(" - MkDocs configuration created") - print(" - Documentation structure created") - print(" - GitHub Actions workflow added") - print(" - Dependencies added to pyproject.toml") - - -def main() -> None: - print("πŸš€ Initializing new Python project...") - - # Get project information - project_name = prompt_with_default( - "Project name", "my-python-project" - ) - project_description = prompt_with_default( - "Project description", "A Python project" - ) - author_name = prompt_with_default( - "Author name", get_git_config("name") or "Your Name" - ) - author_email = prompt_with_default( - "Author email", get_git_config("email") or "your.email@example.com" - ) - - # Update project information - print("πŸ“ Updating project configuration...") - update_pyproject_toml( - project_name, - project_description, - author_name, - author_email - ) - - # Handle example code - code_choice = prompt_with_default( - "How would you like to handle example code?\n" - "1. Keep example code (useful for reference)\n" - "2. Create minimal placeholder test (ensures checks pass)\n" - "3. Remove all example code (clean slate)\n" - "Choose option (1/2/3)", "1" - ) - - # Create module directory with project name (replacing src) - project_module_name = project_name.replace("-", "_").lower() - - # Always update the Makefile to use the new module name - print(f"πŸ”§ Updating Makefile to use module name: {project_module_name}") - makefile_path = Path("Makefile") - with open(makefile_path, "r") as f: - makefile_content = f.read() - - # Replace module name in Makefile - updated_makefile = makefile_content.replace("MODULE_NAME := src", f"MODULE_NAME := {project_module_name}") - - with open(makefile_path, "w") as f: - f.write(updated_makefile) - - # Always update pyproject.toml to point to the new module directory - print(f"πŸ“¦ Updating pyproject.toml for module: {project_module_name}") - pyproject_path = Path("pyproject.toml") - with open(pyproject_path, "rb") as f: - config = tomli.load(f) - - # Update packages from src to new module name - if "tool" in config and "hatch" in config["tool"] and "build" in config["tool"]["hatch"] and "targets" in config["tool"]["hatch"]["build"] and "wheel" in config["tool"]["hatch"]["build"]["targets"]: - config["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"] = [project_module_name] - - # Update ruff known-first-party if ruff config exists - if "tool" in config and "ruff" in config["tool"] and "lint" in config["tool"]["ruff"] and "isort" in config["tool"]["ruff"]["lint"]: - config["tool"]["ruff"]["lint"]["isort"]["known-first-party"] = [project_module_name] - - with open(pyproject_path, "wb") as f: - tomli_w.dump(config, f) - - # Create the new module directory if it doesn't exist - if not os.path.exists(project_module_name): - print(f"πŸ“ Creating module directory: {project_module_name}") - os.mkdir(project_module_name) - # Create __init__.py - with open(f"{project_module_name}/__init__.py", "w") as f: - f.write(f'"""Main package for {project_name}."""\n') - - # Copy src content to new module directory if src exists - if os.path.exists("src") and project_module_name != "src": - print(f"πŸ“¦ Copying content from src to {project_module_name}...") - for item in os.listdir("src"): - src_path = os.path.join("src", item) - dest_path = os.path.join(project_module_name, item) - - if os.path.isfile(src_path): - with open(src_path, "r") as src_file: - content = src_file.read() - with open(dest_path, "w") as dest_file: - dest_file.write(content) - - # Remove the old src directory after copying - print("πŸ—‘οΈ Removing old src directory...") - run_command("rm -rf src") - - if code_choice == "2": - print("πŸ“ Creating minimal placeholder test...") - # Create minimal module - with open(f"{project_module_name}/example.py", "w") as f: - f.write("""def add(a: int, b: int) -> int: - \"\"\"Add two numbers.\"\"\" - return a + b -""") - - # Create minimal test - with open("tests/test_example.py", "w") as f: - f.write(f"""from {project_module_name}.example import add - -def test_add(): - assert add(1, 2) == 3 -""") - elif code_choice == "3": - print("🧹 Removing all example code...") - run_command("make clean-example") - # Create __init__.py in tests - with open("tests/__init__.py", "w") as f: - f.write("") - else: - print("πŸ“š Updating example code imports for new module name...") - # Update example.py to use new module name - if os.path.exists("src/example.py"): - with open("src/example.py", "r") as f: - example_content = f.read() - # Save it to new module directory - with open(f"{project_module_name}/example.py", "w") as f: - f.write(example_content) - - # Update test imports - if os.path.exists("tests/test_example.py"): - with open("tests/test_example.py", "r") as f: - test_content = f.read() - updated_test = test_content.replace("from src.", f"from {project_module_name}.") - with open("tests/test_example.py", "w") as f: - f.write(updated_test) - - # Documentation setup - docs_choice = prompt_with_default( - "\nWould you like to set up documentation with MkDocs?\n" - "This includes:\n" - "- Material theme documentation site\n" - "- Auto-generated API documentation\n" - "- GitHub Pages deployment\n" - "- Local development server\n" - "\nSet up documentation? (Y/n)", "y" - ) - - docs_enabled = docs_choice.lower() in ('y', 'yes', '') - if docs_enabled: - # Extract GitHub username from git config or use placeholder - try: - github_url = subprocess.check_output(["git", "config", "remote.origin.url"], text=True).strip() - if "github.com" in github_url: - # Extract username from GitHub URL - if github_url.startswith("git@github.com:"): - github_username = github_url.split(":")[1].split("/")[0] - elif github_url.startswith("https://github.com/"): - github_username = github_url.split("/")[3] - else: - github_username = "your-username" - else: - github_username = "your-username" - except (subprocess.CalledProcessError, IndexError): - github_username = "your-username" - - setup_documentation( - project_name, - project_description, - author_name, - author_email, - project_module_name, - github_username - ) - else: - print("⏩ Skipping documentation setup") - - # Get current directory name and handle renaming - current_dir = os.path.basename(os.getcwd()) - if current_dir == "python-collab-template" or current_dir == "python-project-test": - parent_dir = os.path.dirname(os.getcwd()) - new_dir = os.path.join(parent_dir, project_name) - print(f"πŸ“ Renaming project directory to {project_name}...") - if os.path.exists(new_dir): - print(f"⚠️ Directory {project_name} already exists. Keeping current directory name.") - else: - # Update source code directory references in Makefile - makefile_path = Path("Makefile") - with open(makefile_path, "r") as f: - makefile_content = f.read() - - # Replace any hardcoded references to python-collab-template in the Makefile - updated_makefile = makefile_content.replace("python-collab-template", project_name) - - with open(makefile_path, "w") as f: - f.write(updated_makefile) - - # Now rename the directory - os.chdir(parent_dir) - os.rename(current_dir, project_name) - os.chdir(project_name) - - # Install dependencies and set up environment - print("πŸ”¨ Setting up development environment...") - run_command("make setup") - - # Configure pre-commit hooks - precommit_choice = prompt_with_default( - "\nWould you like to enable pre-commit hooks?\n" - "These hooks run automatically before each commit to ensure code quality:\n" - "- Type checking (ty)\n" - "- Linting (ruff)\n" - "- Formatting (ruff)\n" - "- Tests (pytest)\n" - "\nEnable pre-commit hooks? (y/n)", "y" - ) - - # Initialize new git repository - print("πŸ”„ Initializing git repository...") - if os.path.exists(".git"): - run_command("rm -rf .git") - run_command("git init") - - if precommit_choice.lower() in ('y', 'yes'): - print("πŸ”§ Setting up pre-commit hooks...") - run_command("uv run pre-commit install") - else: - print("⏩ Skipping pre-commit hooks setup") - - # Initial commit without running pre-commit hooks - run_command("git add .") - run_command('git commit -m "feat: Initial project setup" --no-verify') - - print("✨ Project initialized successfully!") - next_steps = [ - "1. Update README.md with your project details", - "2. Review and update CHANGELOG.md", - f"3. Start adding your code in {project_module_name}/", - "4. Run 'make check' to verify everything works" - ] - - if docs_enabled: - next_steps.extend([ - "5. Serve documentation locally: 'make docs-serve'", - "6. Update docs content in docs/ directory", - "7. Enable GitHub Pages in repository settings for automatic deployment" - ]) - - print("\nNext steps:") - for step in next_steps: - print(step) - - print("\nHappy coding! πŸŽ‰") - - -if __name__ == "__main__": - main() diff --git a/src/__init__.py b/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/example.py b/src/example.py deleted file mode 100644 index 1d82bcd..0000000 --- a/src/example.py +++ /dev/null @@ -1,35 +0,0 @@ -import statistics -from dataclasses import dataclass -from datetime import datetime -from typing import Optional - - -@dataclass -class DataPoint: - """Example data structure for time series data.""" - - timestamp: datetime - value: float - label: Optional[str] = None - - -def calculate_moving_average( - data: list[DataPoint], window_size: int = 3 -) -> list[float]: - """Calculate moving average of values. - - Args: - data: List of DataPoint objects - window_size: Size of moving window - - Returns: - List of moving averages - """ - values = [d.value for d in data] - result = [] - - for i in range(len(values)): - window = values[max(0, i - window_size + 1) : i + 1] - result.append(statistics.mean(window)) - - return result diff --git a/templates/.github/workflows/docs.yml.template b/templates/.github/workflows/docs.yml.template deleted file mode 100644 index 2668105..0000000 --- a/templates/.github/workflows/docs.yml.template +++ /dev/null @@ -1,82 +0,0 @@ -name: Deploy Documentation - -on: - push: - branches: - - main - paths: - - 'docs/**' - - 'mkdocs.yml' - - '.github/workflows/docs.yml' - pull_request: - branches: - - main - paths: - - 'docs/**' - - 'mkdocs.yml' - -permissions: - contents: write - -jobs: - deploy: - runs-on: ubuntu-latest - if: github.event_name == 'push' - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install uv - run: pip install uv - - - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV - - - uses: actions/cache@v4 - with: - key: mkdocs-material-${{ env.cache_id }} - path: .cache - restore-keys: | - mkdocs-material- - - - name: Install dependencies - run: uv sync --group dev - - - name: Deploy documentation - run: uv run mkdocs gh-deploy --force - - build-check: - runs-on: ubuntu-latest - if: github.event_name == 'pull_request' - - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Install uv - run: pip install uv - - - name: Install dependencies - run: uv sync --group dev - - - name: Build documentation - run: uv run mkdocs build --strict - - - name: Check build output - run: | - echo "βœ… Documentation builds successfully" - echo "πŸ“Š Site size: $(du -sh site/ | cut -f1)" - echo "πŸ“„ Pages built: $(find site/ -name "*.html" | wc -l)" \ No newline at end of file diff --git a/templates/docs/getting-started.md.template b/templates/docs/getting-started.md.template deleted file mode 100644 index 78f1534..0000000 --- a/templates/docs/getting-started.md.template +++ /dev/null @@ -1,100 +0,0 @@ -# Getting Started - -This guide will help you get started with {project_name}. - -## Prerequisites - -- Python 3.9 or higher -- Git - -## Installation - -### End Users - -Install from PyPI: -```bash -pip install {project_name} -``` - -### Developers - -1. Clone the repository: - ```bash - git clone https://github.com/{github_username}/{project_name}.git - cd {project_name} - ``` - -2. Set up the development environment: - ```bash - make setup - ``` - -3. Run the tests to verify everything works: - ```bash - make test - ``` - -## Basic Usage - -```python -import {project_module_name} - -# Add your basic usage examples here -``` - -## Development Workflow - -1. Make your changes to the code -2. Add or update tests as needed -3. Run quality checks: - ```bash - make check - ``` -4. Update documentation if needed -5. Commit your changes -6. Create a pull request - -## Available Commands - -Run `make` to see all available commands: - -- `make setup` - Set up development environment -- `make test` - Run tests with coverage -- `make lint` - Run linting -- `make format` - Format code -- `make ty` - Run type checking -- `make check` - Run all quality checks -- `make docs-serve` - Serve documentation locally -- `make docs-build` - Build documentation - -## Testing - -Run the test suite: -```bash -make test -``` - -Run specific tests: -```bash -uv run -m pytest tests/test_specific.py::test_function_name -``` - -## Documentation - -### Viewing Documentation - -Serve documentation locally: -```bash -make docs-serve -``` - -The documentation will be available at http://localhost:8000 - -### Building Documentation - -Build static documentation: -```bash -make docs-build -``` - -The built documentation will be in the `site/` directory. \ No newline at end of file diff --git a/templates/docs/index.md.template b/templates/docs/index.md.template deleted file mode 100644 index 9fd2f92..0000000 --- a/templates/docs/index.md.template +++ /dev/null @@ -1,85 +0,0 @@ -# {project_name} - -{project_description} - -## Features - -- πŸ”§ Modern Python tooling with UV package manager -- πŸ§ͺ Comprehensive testing with pytest -- 🎨 Code formatting with Ruff -- πŸ” Type checking with MyPy -- πŸ“š Documentation with MkDocs + Material -- πŸš€ CI/CD with GitHub Actions -- 🐳 Docker support for development - -## Quick Start - -```bash -# Clone the repository -git clone https://github.com/{github_username}/{project_name}.git -cd {project_name} - -# Set up the development environment -make setup - -# Run quality checks -make check -``` - -## Installation - -### For Users - -```bash -pip install {project_name} -``` - -### For Development - -```bash -# Clone the repository -git clone https://github.com/{github_username}/{project_name}.git -cd {project_name} - -# Set up development environment -make setup - -# Install pre-commit hooks (optional) -make install-hooks -``` - -## Project Structure - -``` -{project_name}/ -β”œβ”€β”€ {project_module_name}/ # Main package -β”œβ”€β”€ tests/ # Test files -β”œβ”€β”€ docs/ # Documentation -β”œβ”€β”€ scripts/ # Utility scripts -β”œβ”€β”€ docker/ # Docker configuration -└── .github/workflows/ # CI/CD automation -``` - -## Usage - -```python -import {project_module_name} - -# Your usage examples here -``` - -## Development - -See the [Getting Started](getting-started.md) guide for detailed development instructions. - -## Contributing - -1. Fork the repository -2. Create a feature branch -3. Make your changes -4. Run the test suite: `make check` -5. Submit a pull request - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. \ No newline at end of file diff --git a/templates/docs/reference/api.md.template b/templates/docs/reference/api.md.template deleted file mode 100644 index 411771e..0000000 --- a/templates/docs/reference/api.md.template +++ /dev/null @@ -1,9 +0,0 @@ -# API Reference - -This page contains the auto-generated API documentation for {project_name}. - -::: {project_module_name} - options: - show_root_heading: true - members_order: source - show_source: false \ No newline at end of file diff --git a/templates/mkdocs.yml.template b/templates/mkdocs.yml.template deleted file mode 100644 index aceb7cc..0000000 --- a/templates/mkdocs.yml.template +++ /dev/null @@ -1,80 +0,0 @@ -site_name: {project_name} -site_url: https://{github_username}.github.io/{project_name}/ -site_description: {project_description} -site_author: {author_name} - -repo_name: {github_username}/{project_name} -repo_url: https://github.com/{github_username}/{project_name} -edit_uri: edit/main/docs/ - -theme: - name: material - features: - - navigation.tabs - - navigation.tabs.sticky - - navigation.sections - - navigation.top - - navigation.tracking - - search.suggest - - search.highlight - - search.share - - toc.follow - - content.action.edit - - content.code.copy - - content.code.annotate - palette: - - scheme: default - primary: indigo - accent: indigo - toggle: - icon: material/brightness-7 - name: Switch to dark mode - - scheme: slate - primary: indigo - accent: indigo - toggle: - icon: material/brightness-4 - name: Switch to light mode - font: - text: Roboto - code: Roboto Mono - icon: - logo: material/library - -plugins: - - search - - mkdocstrings: - handlers: - python: - paths: [{project_module_name}] - options: - show_source: true - show_root_heading: true - merge_init_into_class: true - docstring_style: google - show_signature_annotations: true - separate_signature: true - -markdown_extensions: - - admonition - - attr_list - - pymdownx.details - - pymdownx.superfences - - pymdownx.tabbed: - alternate_style: true - - pymdownx.highlight: - anchor_linenums: true - - pymdownx.inlinehilite - - toc: - permalink: true - -nav: - - Home: index.md - - Getting Started: getting-started.md - - Reference: - - API Documentation: reference/api.md - -extra: - social: - - icon: fontawesome/brands/github - link: https://github.com/{github_username}/{project_name} \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_docs_setup.py b/tests/test_docs_setup.py deleted file mode 100644 index 71d70ae..0000000 --- a/tests/test_docs_setup.py +++ /dev/null @@ -1,136 +0,0 @@ -"""Test MkDocs documentation setup and configuration.""" - -import subprocess -import tomli -from pathlib import Path - - -def test_mkdocs_dependencies_configured(): - """Test that MkDocs dependencies are configured in pyproject.toml.""" - pyproject_path = Path("pyproject.toml") - assert pyproject_path.exists(), "pyproject.toml should exist" - - with open(pyproject_path, "rb") as f: - config = tomli.load(f) - - # Check for dependency groups with docs dependencies (now conditional) - if "dependency-groups" in config and "dev" in config["dependency-groups"]: - dev_deps = config["dependency-groups"]["dev"] - - # Check for MkDocs dependencies - mkdocs_deps = [dep for dep in dev_deps if "mkdocs" in dep.lower()] - - if mkdocs_deps: # Only check if docs deps are present - assert len(mkdocs_deps) >= 2, "Should have mkdocs-material and mkdocstrings dependencies" - - # Verify specific dependencies - has_material = any("mkdocs-material" in dep for dep in dev_deps) - has_mkdocstrings = any("mkdocstrings" in dep for dep in dev_deps) - - assert has_material, "Should have mkdocs-material dependency" - assert has_mkdocstrings, "Should have mkdocstrings dependency" - - -def test_mkdocs_config_exists(): - """Test that mkdocs.yml configuration file exists (if docs enabled).""" - mkdocs_config = Path("mkdocs.yml") - if mkdocs_config.exists(): - # If it exists, it should be valid - assert mkdocs_config.is_file(), "mkdocs.yml should be a file" - - -def test_mkdocs_config_valid(): - """Test that mkdocs.yml has valid configuration (if it exists).""" - mkdocs_config = Path("mkdocs.yml") - if not mkdocs_config.exists(): - return # Skip if docs not enabled - - import yaml - - with open(mkdocs_config, "r") as f: - config = yaml.safe_load(f) - - # Check required fields - assert "site_name" in config, "mkdocs.yml should have site_name" - assert "theme" in config, "mkdocs.yml should have theme configuration" - assert config["theme"]["name"] == "material", "Should use Material theme" - assert "plugins" in config, "mkdocs.yml should have plugins" - - # Check for required plugins - plugin_names = [] - for plugin in config["plugins"]: - if isinstance(plugin, dict): - plugin_names.extend(plugin.keys()) - else: - plugin_names.append(plugin) - - assert "search" in plugin_names, "Should have search plugin" - assert "mkdocstrings" in plugin_names, "Should have mkdocstrings plugin" - - -def test_documentation_structure_exists(): - """Test that basic documentation structure exists (if docs enabled).""" - docs_dir = Path("docs") - if not docs_dir.exists(): - return # Skip if docs not enabled - - assert docs_dir.is_dir(), "docs should be a directory" - - # Check for essential documentation files - index_file = docs_dir / "index.md" - assert index_file.exists(), "docs/index.md should exist" - - -def test_makefile_has_docs_targets(): - """Test that Makefile contains documentation targets.""" - makefile_path = Path("Makefile") - assert makefile_path.exists(), "Makefile should exist" - - with open(makefile_path, "r") as f: - makefile_content = f.read() - - # Check for documentation targets - assert "docs-install:" in makefile_content, "Makefile should have docs-install target" - assert "docs-build:" in makefile_content, "Makefile should have docs-build target" - assert "docs-serve:" in makefile_content, "Makefile should have docs-serve target" - assert "docs-check:" in makefile_content, "Makefile should have docs-check target" - assert "docs-clean:" in makefile_content, "Makefile should have docs-clean target" - - -def test_github_actions_docs_workflow(): - """Test that GitHub Actions workflow for docs exists (if docs enabled).""" - workflow_path = Path(".github/workflows/docs.yml") - if not workflow_path.exists(): - return # Skip if docs not enabled - - import yaml - with open(workflow_path, "r") as f: - workflow = yaml.safe_load(f) - - # Note: YAML parses "on" as boolean True, not string "on" - assert True in workflow or "on" in workflow, "Workflow should have trigger configuration" - assert "jobs" in workflow, "Workflow should have jobs" - - # Check for deployment job - jobs = workflow["jobs"] - assert any("deploy" in job_name.lower() or "docs" in job_name.lower() - for job_name in jobs.keys()), "Should have a docs deployment job" - - -def test_template_files_exist(): - """Test that template files exist for documentation setup.""" - template_dir = Path("templates") - assert template_dir.exists(), "templates/ directory should exist" - - # Check for essential template files - template_files = [ - "mkdocs.yml.template", - "docs/index.md.template", - "docs/getting-started.md.template", - "docs/reference/api.md.template", - ".github/workflows/docs.yml.template" - ] - - for template_file in template_files: - template_path = template_dir / template_file - assert template_path.exists(), f"Template file {template_file} should exist" \ No newline at end of file diff --git a/tests/test_example.py b/tests/test_example.py deleted file mode 100644 index 62a0da6..0000000 --- a/tests/test_example.py +++ /dev/null @@ -1,13 +0,0 @@ -from datetime import datetime -from src.example import DataPoint, calculate_moving_average - -def test_moving_average(): - data = [ - DataPoint(datetime(2024, 1, 1), 1.0, "A"), - DataPoint(datetime(2024, 1, 2), 2.0, "B"), - DataPoint(datetime(2024, 1, 3), 3.0, "C"), - DataPoint(datetime(2024, 1, 4), 4.0, "D"), - ] - - result = calculate_moving_average(data, window_size=2) - assert result == [1.0, 1.5, 2.5, 3.5] diff --git a/uv.lock b/uv.lock deleted file mode 100644 index c21e272..0000000 --- a/uv.lock +++ /dev/null @@ -1,998 +0,0 @@ -version = 1 -revision = 2 -requires-python = ">=3.9" -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version < '3.10'", -] - -[[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, upload-time = "2025-02-01T15:17:41.026Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, -] - -[[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, upload-time = "2025-02-25T18:15:32.003Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload-time = "2025-02-25T16:53:14.607Z" }, - { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload-time = "2025-02-25T16:53:17.266Z" }, - { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload-time = "2025-02-25T16:53:26.378Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload-time = "2025-02-25T16:53:28.167Z" }, - { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, -] - -[[package]] -name = "certifi" -version = "2025.6.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, - { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, - { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, - { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, - { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, - { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, - { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, - { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, - { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, - { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, - { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, - { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, - { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, - { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, - { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, - { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, - { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, - { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, - { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, - { url = "https://files.pythonhosted.org/packages/28/f8/dfb01ff6cc9af38552c69c9027501ff5a5117c4cc18dcd27cb5259fa1888/charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", size = 201671, upload-time = "2025-05-02T08:34:12.696Z" }, - { url = "https://files.pythonhosted.org/packages/32/fb/74e26ee556a9dbfe3bd264289b67be1e6d616329403036f6507bb9f3f29c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", size = 144744, upload-time = "2025-05-02T08:34:14.665Z" }, - { url = "https://files.pythonhosted.org/packages/ad/06/8499ee5aa7addc6f6d72e068691826ff093329fe59891e83b092ae4c851c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", size = 154993, upload-time = "2025-05-02T08:34:17.134Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a2/5e4c187680728219254ef107a6949c60ee0e9a916a5dadb148c7ae82459c/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", size = 147382, upload-time = "2025-05-02T08:34:19.081Z" }, - { url = "https://files.pythonhosted.org/packages/4c/fe/56aca740dda674f0cc1ba1418c4d84534be51f639b5f98f538b332dc9a95/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", size = 149536, upload-time = "2025-05-02T08:34:21.073Z" }, - { url = "https://files.pythonhosted.org/packages/53/13/db2e7779f892386b589173dd689c1b1e304621c5792046edd8a978cbf9e0/charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", size = 151349, upload-time = "2025-05-02T08:34:23.193Z" }, - { url = "https://files.pythonhosted.org/packages/69/35/e52ab9a276186f729bce7a0638585d2982f50402046e4b0faa5d2c3ef2da/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", size = 146365, upload-time = "2025-05-02T08:34:25.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/d8/af7333f732fc2e7635867d56cb7c349c28c7094910c72267586947561b4b/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", size = 154499, upload-time = "2025-05-02T08:34:27.359Z" }, - { url = "https://files.pythonhosted.org/packages/7a/3d/a5b2e48acef264d71e036ff30bcc49e51bde80219bb628ba3e00cf59baac/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", size = 157735, upload-time = "2025-05-02T08:34:29.798Z" }, - { url = "https://files.pythonhosted.org/packages/85/d8/23e2c112532a29f3eef374375a8684a4f3b8e784f62b01da931186f43494/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", size = 154786, upload-time = "2025-05-02T08:34:31.858Z" }, - { url = "https://files.pythonhosted.org/packages/c7/57/93e0169f08ecc20fe82d12254a200dfaceddc1c12a4077bf454ecc597e33/charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", size = 150203, upload-time = "2025-05-02T08:34:33.88Z" }, - { url = "https://files.pythonhosted.org/packages/2c/9d/9bf2b005138e7e060d7ebdec7503d0ef3240141587651f4b445bdf7286c2/charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", size = 98436, upload-time = "2025-05-02T08:34:35.907Z" }, - { url = "https://files.pythonhosted.org/packages/6d/24/5849d46cf4311bbf21b424c443b09b459f5b436b1558c04e45dbb7cc478b/charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", size = 105772, upload-time = "2025-05-02T08:34:37.935Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" }, -] - -[[package]] -name = "click" -version = "8.2.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "coverage" -version = "7.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/78/1c1c5ec58f16817c09cbacb39783c3655d54a221b6552f47ff5ac9297603/coverage-7.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cc94d7c5e8423920787c33d811c0be67b7be83c705f001f7180c7b186dcf10ca", size = 212028, upload-time = "2025-06-13T13:00:29.293Z" }, - { url = "https://files.pythonhosted.org/packages/98/db/e91b9076f3a888e3b4ad7972ea3842297a52cc52e73fd1e529856e473510/coverage-7.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16aa0830d0c08a2c40c264cef801db8bc4fc0e1892782e45bcacbd5889270509", size = 212420, upload-time = "2025-06-13T13:00:34.027Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d0/2b3733412954576b0aea0a16c3b6b8fbe95eb975d8bfa10b07359ead4252/coverage-7.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf95981b126f23db63e9dbe4cf65bd71f9a6305696fa5e2262693bc4e2183f5b", size = 241529, upload-time = "2025-06-13T13:00:35.786Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/5e2e5ae2e750a872226a68e984d4d3f3563cb01d1afb449a17aa819bc2c4/coverage-7.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f05031cf21699785cd47cb7485f67df619e7bcdae38e0fde40d23d3d0210d3c3", size = 239403, upload-time = "2025-06-13T13:00:37.399Z" }, - { url = "https://files.pythonhosted.org/packages/37/3b/a2c27736035156b0a7c20683afe7df498480c0dfdf503b8c878a21b6d7fb/coverage-7.9.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb4fbcab8764dc072cb651a4bcda4d11fb5658a1d8d68842a862a6610bd8cfa3", size = 240548, upload-time = "2025-06-13T13:00:39.647Z" }, - { url = "https://files.pythonhosted.org/packages/98/f5/13d5fc074c3c0e0dc80422d9535814abf190f1254d7c3451590dc4f8b18c/coverage-7.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0f16649a7330ec307942ed27d06ee7e7a38417144620bb3d6e9a18ded8a2d3e5", size = 240459, upload-time = "2025-06-13T13:00:40.934Z" }, - { url = "https://files.pythonhosted.org/packages/36/24/24b9676ea06102df824c4a56ffd13dc9da7904478db519efa877d16527d5/coverage-7.9.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cea0a27a89e6432705fffc178064503508e3c0184b4f061700e771a09de58187", size = 239128, upload-time = "2025-06-13T13:00:42.343Z" }, - { url = "https://files.pythonhosted.org/packages/be/05/242b7a7d491b369ac5fee7908a6e5ba42b3030450f3ad62c645b40c23e0e/coverage-7.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e980b53a959fa53b6f05343afbd1e6f44a23ed6c23c4b4c56c6662bbb40c82ce", size = 239402, upload-time = "2025-06-13T13:00:43.634Z" }, - { url = "https://files.pythonhosted.org/packages/73/e0/4de7f87192fa65c9c8fbaeb75507e124f82396b71de1797da5602898be32/coverage-7.9.1-cp310-cp310-win32.whl", hash = "sha256:70760b4c5560be6ca70d11f8988ee6542b003f982b32f83d5ac0b72476607b70", size = 214518, upload-time = "2025-06-13T13:00:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ab/5e4e2fe458907d2a65fab62c773671cfc5ac704f1e7a9ddd91996f66e3c2/coverage-7.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a66e8f628b71f78c0e0342003d53b53101ba4e00ea8dabb799d9dba0abbbcebe", size = 215436, upload-time = "2025-06-13T13:00:47.245Z" }, - { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, - { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, - { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, - { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, - { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/d0/a7/a027970c991ca90f24e968999f7d509332daf6b8c3533d68633930aaebac/coverage-7.9.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:31324f18d5969feef7344a932c32428a2d1a3e50b15a6404e97cba1cc9b2c631", size = 212358, upload-time = "2025-06-13T13:01:30.909Z" }, - { url = "https://files.pythonhosted.org/packages/f2/48/6aaed3651ae83b231556750280682528fea8ac7f1232834573472d83e459/coverage-7.9.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c804506d624e8a20fb3108764c52e0eef664e29d21692afa375e0dd98dc384f", size = 212620, upload-time = "2025-06-13T13:01:32.256Z" }, - { url = "https://files.pythonhosted.org/packages/6c/2a/f4b613f3b44d8b9f144847c89151992b2b6b79cbc506dee89ad0c35f209d/coverage-7.9.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef64c27bc40189f36fcc50c3fb8f16ccda73b6a0b80d9bd6e6ce4cffcd810bbd", size = 245788, upload-time = "2025-06-13T13:01:33.948Z" }, - { url = "https://files.pythonhosted.org/packages/04/d2/de4fdc03af5e4e035ef420ed26a703c6ad3d7a07aff2e959eb84e3b19ca8/coverage-7.9.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4fe2348cc6ec372e25adec0219ee2334a68d2f5222e0cba9c0d613394e12d86", size = 243001, upload-time = "2025-06-13T13:01:35.285Z" }, - { url = "https://files.pythonhosted.org/packages/f5/e8/eed18aa5583b0423ab7f04e34659e51101135c41cd1dcb33ac1d7013a6d6/coverage-7.9.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34ed2186fe52fcc24d4561041979a0dec69adae7bce2ae8d1c49eace13e55c43", size = 244985, upload-time = "2025-06-13T13:01:36.712Z" }, - { url = "https://files.pythonhosted.org/packages/17/f8/ae9e5cce8885728c934eaa58ebfa8281d488ef2afa81c3dbc8ee9e6d80db/coverage-7.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:25308bd3d00d5eedd5ae7d4357161f4df743e3c0240fa773ee1b0f75e6c7c0f1", size = 245152, upload-time = "2025-06-13T13:01:39.303Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c8/272c01ae792bb3af9b30fac14d71d63371db227980682836ec388e2c57c0/coverage-7.9.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:73e9439310f65d55a5a1e0564b48e34f5369bee943d72c88378f2d576f5a5751", size = 243123, upload-time = "2025-06-13T13:01:40.727Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/2819a1e3086143c094ab446e3bdf07138527a7b88cb235c488e78150ba7a/coverage-7.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37ab6be0859141b53aa89412a82454b482c81cf750de4f29223d52268a86de67", size = 244506, upload-time = "2025-06-13T13:01:42.184Z" }, - { url = "https://files.pythonhosted.org/packages/8b/4e/9f6117b89152df7b6112f65c7a4ed1f2f5ec8e60c4be8f351d91e7acc848/coverage-7.9.1-cp313-cp313-win32.whl", hash = "sha256:64bdd969456e2d02a8b08aa047a92d269c7ac1f47e0c977675d550c9a0863643", size = 214766, upload-time = "2025-06-13T13:01:44.482Z" }, - { url = "https://files.pythonhosted.org/packages/27/0f/4b59f7c93b52c2c4ce7387c5a4e135e49891bb3b7408dcc98fe44033bbe0/coverage-7.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:be9e3f68ca9edb897c2184ad0eee815c635565dbe7a0e7e814dc1f7cbab92c0a", size = 215568, upload-time = "2025-06-13T13:01:45.772Z" }, - { url = "https://files.pythonhosted.org/packages/09/1e/9679826336f8c67b9c39a359352882b24a8a7aee48d4c9cad08d38d7510f/coverage-7.9.1-cp313-cp313-win_arm64.whl", hash = "sha256:1c503289ffef1d5105d91bbb4d62cbe4b14bec4d13ca225f9c73cde9bb46207d", size = 213939, upload-time = "2025-06-13T13:01:47.087Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5b/5c6b4e7a407359a2e3b27bf9c8a7b658127975def62077d441b93a30dbe8/coverage-7.9.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0b3496922cb5f4215bf5caaef4cf12364a26b0be82e9ed6d050f3352cf2d7ef0", size = 213079, upload-time = "2025-06-13T13:01:48.554Z" }, - { url = "https://files.pythonhosted.org/packages/a2/22/1e2e07279fd2fd97ae26c01cc2186e2258850e9ec125ae87184225662e89/coverage-7.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9565c3ab1c93310569ec0d86b017f128f027cab0b622b7af288696d7ed43a16d", size = 213299, upload-time = "2025-06-13T13:01:49.997Z" }, - { url = "https://files.pythonhosted.org/packages/14/c0/4c5125a4b69d66b8c85986d3321520f628756cf524af810baab0790c7647/coverage-7.9.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2241ad5dbf79ae1d9c08fe52b36d03ca122fb9ac6bca0f34439e99f8327ac89f", size = 256535, upload-time = "2025-06-13T13:01:51.314Z" }, - { url = "https://files.pythonhosted.org/packages/81/8b/e36a04889dda9960be4263e95e777e7b46f1bb4fc32202612c130a20c4da/coverage-7.9.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb5838701ca68b10ebc0937dbd0eb81974bac54447c55cd58dea5bca8451029", size = 252756, upload-time = "2025-06-13T13:01:54.403Z" }, - { url = "https://files.pythonhosted.org/packages/98/82/be04eff8083a09a4622ecd0e1f31a2c563dbea3ed848069e7b0445043a70/coverage-7.9.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b30a25f814591a8c0c5372c11ac8967f669b97444c47fd794926e175c4047ece", size = 254912, upload-time = "2025-06-13T13:01:56.769Z" }, - { url = "https://files.pythonhosted.org/packages/0f/25/c26610a2c7f018508a5ab958e5b3202d900422cf7cdca7670b6b8ca4e8df/coverage-7.9.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2d04b16a6062516df97969f1ae7efd0de9c31eb6ebdceaa0d213b21c0ca1a683", size = 256144, upload-time = "2025-06-13T13:01:58.19Z" }, - { url = "https://files.pythonhosted.org/packages/c5/8b/fb9425c4684066c79e863f1e6e7ecebb49e3a64d9f7f7860ef1688c56f4a/coverage-7.9.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7931b9e249edefb07cd6ae10c702788546341d5fe44db5b6108a25da4dca513f", size = 254257, upload-time = "2025-06-13T13:01:59.645Z" }, - { url = "https://files.pythonhosted.org/packages/93/df/27b882f54157fc1131e0e215b0da3b8d608d9b8ef79a045280118a8f98fe/coverage-7.9.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52e92b01041151bf607ee858e5a56c62d4b70f4dac85b8c8cb7fb8a351ab2c10", size = 255094, upload-time = "2025-06-13T13:02:01.37Z" }, - { url = "https://files.pythonhosted.org/packages/41/5f/cad1c3dbed8b3ee9e16fa832afe365b4e3eeab1fb6edb65ebbf745eabc92/coverage-7.9.1-cp313-cp313t-win32.whl", hash = "sha256:684e2110ed84fd1ca5f40e89aa44adf1729dc85444004111aa01866507adf363", size = 215437, upload-time = "2025-06-13T13:02:02.905Z" }, - { url = "https://files.pythonhosted.org/packages/99/4d/fad293bf081c0e43331ca745ff63673badc20afea2104b431cdd8c278b4c/coverage-7.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:437c576979e4db840539674e68c84b3cda82bc824dd138d56bead1435f1cb5d7", size = 216605, upload-time = "2025-06-13T13:02:05.638Z" }, - { url = "https://files.pythonhosted.org/packages/1f/56/4ee027d5965fc7fc126d7ec1187529cc30cc7d740846e1ecb5e92d31b224/coverage-7.9.1-cp313-cp313t-win_arm64.whl", hash = "sha256:18a0912944d70aaf5f399e350445738a1a20b50fbea788f640751c2ed9208b6c", size = 214392, upload-time = "2025-06-13T13:02:07.642Z" }, - { url = "https://files.pythonhosted.org/packages/a5/d6/c41dd9b02bf16ec001aaf1cbef665537606899a3db1094e78f5ae17540ca/coverage-7.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f424507f57878e424d9a95dc4ead3fbdd72fd201e404e861e465f28ea469951", size = 212029, upload-time = "2025-06-13T13:02:09.058Z" }, - { url = "https://files.pythonhosted.org/packages/f8/c0/40420d81d731f84c3916dcdf0506b3e6c6570817bff2576b83f780914ae6/coverage-7.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:535fde4001b2783ac80865d90e7cc7798b6b126f4cd8a8c54acfe76804e54e58", size = 212407, upload-time = "2025-06-13T13:02:11.151Z" }, - { url = "https://files.pythonhosted.org/packages/9b/87/f0db7d62d0e09f14d6d2f6ae8c7274a2f09edf74895a34b412a0601e375a/coverage-7.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02532fd3290bb8fa6bec876520842428e2a6ed6c27014eca81b031c2d30e3f71", size = 241160, upload-time = "2025-06-13T13:02:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/a9/b7/3337c064f058a5d7696c4867159651a5b5fb01a5202bcf37362f0c51400e/coverage-7.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56f5eb308b17bca3bbff810f55ee26d51926d9f89ba92707ee41d3c061257e55", size = 239027, upload-time = "2025-06-13T13:02:14.294Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a9/5898a283f66d1bd413c32c2e0e05408196fd4f37e206e2b06c6e0c626e0e/coverage-7.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfa447506c1a52271f1b0de3f42ea0fa14676052549095e378d5bff1c505ff7b", size = 240145, upload-time = "2025-06-13T13:02:15.745Z" }, - { url = "https://files.pythonhosted.org/packages/e0/33/d96e3350078a3c423c549cb5b2ba970de24c5257954d3e4066e2b2152d30/coverage-7.9.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9ca8e220006966b4a7b68e8984a6aee645a0384b0769e829ba60281fe61ec4f7", size = 239871, upload-time = "2025-06-13T13:02:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/1d/6e/6fb946072455f71a820cac144d49d11747a0f1a21038060a68d2d0200499/coverage-7.9.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:49f1d0788ba5b7ba65933f3a18864117c6506619f5ca80326b478f72acf3f385", size = 238122, upload-time = "2025-06-13T13:02:18.849Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5c/bc43f25c8586840ce25a796a8111acf6a2b5f0909ba89a10d41ccff3920d/coverage-7.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68cd53aec6f45b8e4724c0950ce86eacb775c6be01ce6e3669fe4f3a21e768ed", size = 239058, upload-time = "2025-06-13T13:02:21.423Z" }, - { url = "https://files.pythonhosted.org/packages/11/d8/ce2007418dd7fd00ff8c8b898bb150bb4bac2d6a86df05d7b88a07ff595f/coverage-7.9.1-cp39-cp39-win32.whl", hash = "sha256:95335095b6c7b1cc14c3f3f17d5452ce677e8490d101698562b2ffcacc304c8d", size = 214532, upload-time = "2025-06-13T13:02:22.857Z" }, - { url = "https://files.pythonhosted.org/packages/20/21/334e76fa246e92e6d69cab217f7c8a70ae0cc8f01438bd0544103f29528e/coverage-7.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:e1b5191d1648acc439b24721caab2fd0c86679d8549ed2c84d5a7ec1bedcc244", size = 215439, upload-time = "2025-06-13T13:02:24.268Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, -] - -[[package]] -name = "distlib" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, -] - -[[package]] -name = "filelock" -version = "3.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, -] - -[[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, upload-time = "2022-05-02T15:47:16.11Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, -] - -[[package]] -name = "griffe" -version = "1.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" }, -] - -[[package]] -name = "identify" -version = "2.6.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/88/d193a27416618628a5eea64e3223acd800b40749a96ffb322a9b55a49ed1/identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6", size = 99254, upload-time = "2025-05-23T20:37:53.3Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/cd/18f8da995b658420625f7ef13f037be53ae04ec5ad33f9b718240dcfd48c/identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2", size = 99145, upload-time = "2025-05-23T20:37:51.495Z" }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, -] - -[[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 = "markdown" -version = "3.8.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, - { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, - { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, - { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, - { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, - { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, - { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, - { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, - { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, - { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, - { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, - { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, - { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, - { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, - { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, - { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, - { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, - { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, - { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, - { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, - { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, - { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, - { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, - { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, - { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, - { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, - { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, - { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, - { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, - { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, - { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, - { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ea/9b1530c3fdeeca613faeb0fb5cbcf2389d816072fab72a71b45749ef6062/MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", size = 14344, upload-time = "2024-10-18T15:21:43.721Z" }, - { url = "https://files.pythonhosted.org/packages/4b/c2/fbdbfe48848e7112ab05e627e718e854d20192b674952d9042ebd8c9e5de/MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", size = 12389, upload-time = "2024-10-18T15:21:44.666Z" }, - { url = "https://files.pythonhosted.org/packages/f0/25/7a7c6e4dbd4f867d95d94ca15449e91e52856f6ed1905d58ef1de5e211d0/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", size = 21607, upload-time = "2024-10-18T15:21:45.452Z" }, - { url = "https://files.pythonhosted.org/packages/53/8f/f339c98a178f3c1e545622206b40986a4c3307fe39f70ccd3d9df9a9e425/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", size = 20728, upload-time = "2024-10-18T15:21:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/1a/03/8496a1a78308456dbd50b23a385c69b41f2e9661c67ea1329849a598a8f9/MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", size = 20826, upload-time = "2024-10-18T15:21:47.134Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cf/0a490a4bd363048c3022f2f475c8c05582179bb179defcee4766fb3dcc18/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", size = 21843, upload-time = "2024-10-18T15:21:48.334Z" }, - { url = "https://files.pythonhosted.org/packages/19/a3/34187a78613920dfd3cdf68ef6ce5e99c4f3417f035694074beb8848cd77/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", size = 21219, upload-time = "2024-10-18T15:21:49.587Z" }, - { url = "https://files.pythonhosted.org/packages/17/d8/5811082f85bb88410ad7e452263af048d685669bbbfb7b595e8689152498/MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", size = 20946, upload-time = "2024-10-18T15:21:50.441Z" }, - { url = "https://files.pythonhosted.org/packages/7c/31/bd635fb5989440d9365c5e3c47556cfea121c7803f5034ac843e8f37c2f2/MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", size = 15063, upload-time = "2024-10-18T15:21:51.385Z" }, - { url = "https://files.pythonhosted.org/packages/b3/73/085399401383ce949f727afec55ec3abd76648d04b9f22e1c0e99cb4bec3/MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", size = 15506, upload-time = "2024-10-18T15:21:52.974Z" }, -] - -[[package]] -name = "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, upload-time = "2021-02-05T18:55:30.623Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, -] - -[[package]] -name = "mkdocs" -version = "1.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "ghp-import" }, - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { 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, upload-time = "2024-08-30T12:24:06.899Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, -] - -[[package]] -name = "mkdocs-autorefs" -version = "1.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "markupsafe" }, - { name = "mkdocs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/47/0c/c9826f35b99c67fa3a7cddfa094c1a6c43fafde558c309c6e4403e5b37dc/mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749", size = 54961, upload-time = "2025-05-20T13:09:09.886Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/dc/fc063b78f4b769d1956319351704e23ebeba1e9e1d6a41b4b602325fd7e4/mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13", size = 24969, upload-time = "2025-05-20T13:09:08.237Z" }, -] - -[[package]] -name = "mkdocs-get-deps" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { 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, upload-time = "2023-11-20T17:51:09.981Z" } -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, upload-time = "2023-11-20T17:51:08.587Z" }, -] - -[[package]] -name = "mkdocs-material" -version = "9.6.14" -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/b3/fa/0101de32af88f87cf5cc23ad5f2e2030d00995f74e616306513431b8ab4b/mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754", size = 3951707, upload-time = "2025-05-13T13:27:57.173Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/a1/7fdb959ad592e013c01558822fd3c22931a95a0f08cf0a7c36da13a5b2b5/mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b", size = 8703767, upload-time = "2025-05-13T13:27:54.089Z" }, -] - -[[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, upload-time = "2023-11-22T19:09:45.208Z" } -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, upload-time = "2023-11-22T19:09:43.465Z" }, -] - -[[package]] -name = "mkdocstrings" -version = "0.29.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, - { 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, upload-time = "2025-03-31T08:33:11.997Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/14/22533a578bf8b187e05d67e2c1721ce10e3f526610eebaf7a149d557ea7a/mkdocstrings-0.29.1-py3-none-any.whl", hash = "sha256:37a9736134934eea89cbd055a513d40a020d87dfcae9e3052c2a6b8cd4af09b6", size = 1631075, upload-time = "2025-03-31T08:33:09.661Z" }, -] - -[package.optional-dependencies] -python = [ - { name = "mkdocstrings-python" }, -] - -[[package]] -name = "mkdocstrings-python" -version = "1.16.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mkdocs-autorefs" }, - { name = "mkdocstrings" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bf/ed/b886f8c714fd7cccc39b79646b627dbea84cd95c46be43459ef46852caf0/mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d", size = 206065, upload-time = "2025-06-03T12:52:49.276Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/dd/a24ee3de56954bfafb6ede7cd63c2413bb842cc48eb45e41c43a05a33074/mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374", size = 124287, upload-time = "2025-06-03T12:52:47.819Z" }, -] - -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "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, upload-time = "2024-08-25T14:17:24.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.3.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pre-commit" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424, upload-time = "2025-03-18T21:35:20.987Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707, upload-time = "2025-03-18T21:35:19.343Z" }, -] - -[[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, upload-time = "2025-01-06T17:26:30.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, -] - -[[package]] -name = "pymdown-extensions" -version = "10.15" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/92/a7296491dbf5585b3a987f3f3fc87af0e632121ff3e490c14b5f2d2b4eb5/pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7", size = 852320, upload-time = "2025-04-27T23:48:29.183Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/d1/c54e608505776ce4e7966d03358ae635cfd51dff1da6ee421c090dbc797b/pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f", size = 265845, upload-time = "2025-04-27T23:48:27.359Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, -] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, -] - -[[package]] -name = "python-collab-template" -version = "0.1.0" -source = { editable = "." } - -[package.optional-dependencies] -dev = [ - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-cov" }, - { name = "ruff" }, - { name = "tomli" }, - { name = "tomli-w" }, - { name = "ty" }, -] - -[package.dev-dependencies] -dev = [ - { name = "mkdocs-material" }, - { name = "mkdocstrings", extra = ["python"] }, -] - -[package.metadata] -requires-dist = [ - { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.6.0" }, - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.1.1" }, - { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" }, - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, - { name = "tomli", marker = "extra == 'dev'", specifier = ">=2.0.1" }, - { name = "tomli-w", marker = "extra == 'dev'", specifier = ">=1.0.0" }, - { name = "ty", marker = "extra == 'dev'", specifier = ">=0.0.2" }, -] -provides-extras = ["dev"] - -[package.metadata.requires-dev] -dev = [ - { name = "mkdocs-material", specifier = ">=9.6.14" }, - { name = "mkdocstrings", extras = ["python"], specifier = ">=0.26.1" }, -] - -[[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, upload-time = "2024-03-01T18:36:20.211Z" } -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, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, - { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, - { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, - { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, - { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, - { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, - { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, - { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, - { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, - { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, - { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, - { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, - { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, - { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, - { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, - { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, - { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, - { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, - { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, - { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, - { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, - { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, - { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, - { url = "https://files.pythonhosted.org/packages/65/d8/b7a1db13636d7fb7d4ff431593c510c8b8fca920ade06ca8ef20015493c5/PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", size = 184777, upload-time = "2024-08-06T20:33:25.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/02/6ec546cd45143fdf9840b2c6be8d875116a64076218b61d68e12548e5839/PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", size = 172318, upload-time = "2024-08-06T20:33:27.212Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9a/8cc68be846c972bda34f6c2a93abb644fb2476f4dcc924d52175786932c9/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", size = 720891, upload-time = "2024-08-06T20:33:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6c/6e1b7f40181bc4805e2e07f4abc10a88ce4648e7e95ff1abe4ae4014a9b2/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", size = 722614, upload-time = "2024-08-06T20:33:34.157Z" }, - { url = "https://files.pythonhosted.org/packages/3d/32/e7bd8535d22ea2874cef6a81021ba019474ace0d13a4819c2a4bce79bd6a/PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", size = 737360, upload-time = "2024-08-06T20:33:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/d7/12/7322c1e30b9be969670b672573d45479edef72c9a0deac3bb2868f5d7469/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", size = 699006, upload-time = "2024-08-06T20:33:37.501Z" }, - { url = "https://files.pythonhosted.org/packages/82/72/04fcad41ca56491995076630c3ec1e834be241664c0c09a64c9a2589b507/PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", size = 723577, upload-time = "2024-08-06T20:33:39.389Z" }, - { url = "https://files.pythonhosted.org/packages/ed/5e/46168b1f2757f1fcd442bc3029cd8767d88a98c9c05770d8b420948743bb/PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", size = 144593, upload-time = "2024-08-06T20:33:46.63Z" }, - { url = "https://files.pythonhosted.org/packages/19/87/5124b1c1f2412bb95c59ec481eaf936cd32f0fe2a7b16b97b81c4c017a6a/PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", size = 162312, upload-time = "2024-08-06T20:33:49.073Z" }, -] - -[[package]] -name = "pyyaml-env-tag" -version = "1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, -] - -[[package]] -name = "requests" -version = "2.32.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, -] - -[[package]] -name = "ruff" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, - { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, - { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, -] - -[[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, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, - { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, - { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, - { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, - { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, - { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, - { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, -] - -[[package]] -name = "tomli-w" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" }, -] - -[[package]] -name = "ty" -version = "0.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/e5/15b6aceefcd64b53997fe2002b6fa055f0b1afd23ff6fc3f55f3da944530/ty-0.0.2.tar.gz", hash = "sha256:e02dc50b65dc58d6cb8e8b0d563833f81bf03ed8a7d0b15c6396d486489a7e1d", size = 4762024, upload-time = "2025-12-16T20:13:41.07Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6b/86/65d4826677d966cf226662767a4a597ebb4b02c432f413673c8d5d3d1ce8/ty-0.0.2-py3-none-linux_armv6l.whl", hash = "sha256:0954a0e0b6f7e06229dd1da3a9989ee9b881a26047139a88eb7c134c585ad22e", size = 9771409, upload-time = "2025-12-16T20:13:28.964Z" }, - { url = "https://files.pythonhosted.org/packages/d4/bc/6ab06b7c109cec608c24ea182cc8b4714e746a132f70149b759817092665/ty-0.0.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d6044b491d66933547033cecc87cb7eb599ba026a3ef347285add6b21107a648", size = 9580025, upload-time = "2025-12-16T20:13:34.507Z" }, - { url = "https://files.pythonhosted.org/packages/54/de/d826804e304b2430f17bb27ae15bcf02380e7f67f38b5033047e3d2523e6/ty-0.0.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbca7f08e671a35229f6f400d73da92e2dc0a440fba53a74fe8233079a504358", size = 9098660, upload-time = "2025-12-16T20:13:01.278Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/5cd87944ceee02bb0826f19ced54e30c6bb971e985a22768f6be6b1a042f/ty-0.0.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3abd61153dac0b93b284d305e6f96085013a25c3a7ab44e988d24f0a5fcce729", size = 9567693, upload-time = "2025-12-16T20:13:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/c6/b1/062aab2c62c5ae01c05d27b97ba022d9ff66f14a3cb9030c5ad1dca797ec/ty-0.0.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:21a9f28caafb5742e7d594104e2fe2ebd64590da31aed4745ae8bc5be67a7b85", size = 9556471, upload-time = "2025-12-16T20:13:07.771Z" }, - { url = "https://files.pythonhosted.org/packages/0e/07/856f6647a9dd6e36560d182d35d3b5fb21eae98a8bfb516cd879d0e509f3/ty-0.0.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3ec63fd23ab48e0f838fb54a47ec362a972ee80979169a7edfa6f5c5034849d", size = 9971914, upload-time = "2025-12-16T20:13:18.852Z" }, - { url = "https://files.pythonhosted.org/packages/2e/82/c2e3957dbf33a23f793a9239cfd8bd04b6defd999bd0f6e74d6a5afb9f42/ty-0.0.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e5e2e0293a259c9a53f668c9c13153cc2f1403cb0fe2b886ca054be4ac76517c", size = 10840905, upload-time = "2025-12-16T20:13:37.098Z" }, - { url = "https://files.pythonhosted.org/packages/3b/17/49bd74e3d577e6c88b8074581b7382f532a9d40552cc7c48ceaa83f1d950/ty-0.0.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2511ac02a83d0dc45d4570c7e21ec0c919be7a7263bad9914800d0cde47817", size = 10570251, upload-time = "2025-12-16T20:13:10.319Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9b/26741834069722033a1a0963fcbb63ea45925c6697357e64e361753c6166/ty-0.0.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c482bfbfb8ad18b2e62427d02a0c934ac510c414188a3cf00e16b8acc35482f0", size = 10369078, upload-time = "2025-12-16T20:13:20.851Z" }, - { url = "https://files.pythonhosted.org/packages/94/fc/1d34ec891900d9337169ff9f8252fcaa633ae5c4d36b67effd849ed4f9ac/ty-0.0.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb514711eed3f56d7a130d4885f4b5d8e490fdcd2adac098e5cf175573a0dda3", size = 10121064, upload-time = "2025-12-16T20:13:23.095Z" }, - { url = "https://files.pythonhosted.org/packages/e5/02/e640325956172355ef8deb9b08d991f229230bf9d07f1dbda8c6665a3a43/ty-0.0.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b2c37fa26c39e9fbed7c73645ba721968ab44f28b2bfe2f79a4e15965a1c426f", size = 9553817, upload-time = "2025-12-16T20:13:27.057Z" }, - { url = "https://files.pythonhosted.org/packages/35/13/c93d579ece84895da9b0aae5d34d84100bbff63ad9f60c906a533a087175/ty-0.0.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:13b264833ac5f3b214693fca38e380e78ee7327e09beaa5ff2e47d75fcab9692", size = 9577512, upload-time = "2025-12-16T20:13:16.956Z" }, - { url = "https://files.pythonhosted.org/packages/85/53/93ab1570adc799cd9120ea187d5b4c00d821e86eca069943b179fe0d3e83/ty-0.0.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:08658d6dbbf8bdef80c0a77eda56a22ab6737002ba129301b7bbd36bcb7acd75", size = 9692726, upload-time = "2025-12-16T20:13:31.169Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/5fff5335858a14196776207d231c32e23e48a5c912a7d52c80e7a3fa6f8f/ty-0.0.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4a21b5b012061cb13d47edfff6be70052694308dba633b4c819b70f840e6c158", size = 10213996, upload-time = "2025-12-16T20:13:14.606Z" }, - { url = "https://files.pythonhosted.org/packages/a0/d3/896b1439ab765c57a8d732f73c105ec41142c417a582600638385c2bee85/ty-0.0.2-py3-none-win32.whl", hash = "sha256:d773fdad5d2b30f26313204e6b191cdd2f41ab440a6c241fdb444f8c6593c288", size = 9204906, upload-time = "2025-12-16T20:13:25.099Z" }, - { url = "https://files.pythonhosted.org/packages/5d/0a/f30981e7d637f78e3d08e77d63b818752d23db1bc4b66f9e82e2cb3d34f8/ty-0.0.2-py3-none-win_amd64.whl", hash = "sha256:d1c9ac78a8aa60d0ce89acdccf56c3cc0fcb2de07f1ecf313754d83518e8e8c5", size = 10066640, upload-time = "2025-12-16T20:13:04.045Z" }, - { url = "https://files.pythonhosted.org/packages/5a/c4/97958503cf62bfb7908d2a77b03b91a20499a7ff405f5a098c4989589f34/ty-0.0.2-py3-none-win_arm64.whl", hash = "sha256:fbdef644ade0cd4420c4ec14b604b7894cefe77bfd8659686ac2f6aba9d1a306", size = 9572022, upload-time = "2025-12-16T20:13:39.189Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, -] - -[[package]] -name = "urllib3" -version = "2.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, -] - -[[package]] -name = "virtualenv" -version = "20.31.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/56/2c/444f465fb2c65f40c3a104fd0c495184c4f2336d65baf398e3c75d72ea94/virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af", size = 6076316, upload-time = "2025-05-08T17:58:23.811Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/40/b1c265d4b2b62b58576588510fc4d1fe60a86319c8de99fd8e9fec617d2c/virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11", size = 6057982, upload-time = "2025-05-08T17:58:21.15Z" }, -] - -[[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, upload-time = "2024-11-01T14:07:13.037Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, - { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, - { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, - { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, - { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, - { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, - { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, - { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, - { 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, upload-time = "2024-11-01T14:06:42.952Z" }, - { 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, upload-time = "2024-11-01T14:06:45.084Z" }, - { 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, upload-time = "2024-11-01T14:06:47.324Z" }, - { url = "https://files.pythonhosted.org/packages/05/52/7223011bb760fce8ddc53416beb65b83a3ea6d7d13738dde75eeb2c89679/watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8", size = 96390, upload-time = "2024-11-01T14:06:49.325Z" }, - { url = "https://files.pythonhosted.org/packages/9c/62/d2b21bc4e706d3a9d467561f487c2938cbd881c69f3808c43ac1ec242391/watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a", size = 88386, upload-time = "2024-11-01T14:06:50.536Z" }, - { url = "https://files.pythonhosted.org/packages/ea/22/1c90b20eda9f4132e4603a26296108728a8bfe9584b006bd05dd94548853/watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c", size = 89017, upload-time = "2024-11-01T14:06:51.717Z" }, - { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, - { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, - { url = "https://files.pythonhosted.org/packages/5b/79/69f2b0e8d3f2afd462029031baafb1b75d11bb62703f0e1022b2e54d49ee/watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa", size = 87903, upload-time = "2024-11-01T14:06:57.052Z" }, - { url = "https://files.pythonhosted.org/packages/e2/2b/dc048dd71c2e5f0f7ebc04dd7912981ec45793a03c0dc462438e0591ba5d/watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e", size = 88381, upload-time = "2024-11-01T14:06:58.193Z" }, - { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, - { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, - { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, - { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, - { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, - { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, - { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, - { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, -] From d02f7d0f7d9c02c4bc3c6bfe581363b7565a1951 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 18:46:07 +0000 Subject: [PATCH 2/4] docs: clarify central-repo usage model in README Add explicit "one-time tag setup" step, explain that target repos only need the two-line caller workflow (everything else runs in this repo), and note that updates here propagate automatically on next tag release. https://claude.ai/code/session_018moumKtzZQiSiyGxVUFGdS --- README.md | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 361d1c5..af1bc29 100644 --- a/README.md +++ b/README.md @@ -14,17 +14,28 @@ Reusable GitHub Actions workflow that runs [Claude Code Action](https://github.c ``` .github/workflows/ - claude_pr_review.yml # Reusable workflow (workflow_call) - ai_pr_review.yml # Caller workflow for this repo (example) + claude_pr_review.yml # Reusable workflow (workflow_call) β€” lives here + ai_pr_review.yml # Caller workflow for this repo (also an example) prompts/ codex_code_review_prompt.md # Official Codex prompt text (verbatim) ``` -## Using this in another repo +## How to use this in another repo -### 1. Add the caller workflow +This repo acts as the **central workflow host**. Any other repo can call the reusable workflow with three lines β€” no workflow code to copy or maintain. -Create `.github/workflows/ai_pr_review.yml` in your target repo: +### 1. Tag this repo first (one-time setup) + +```bash +git tag v1 +git push origin v1 +``` + +Bump to `v1.1`, `v2`, etc. for future breaking changes. + +### 2. Add a two-line caller workflow to each target repo + +Create `.github/workflows/ai_pr_review.yml` in the target repo: ```yaml name: Claude PR scan (Draft->Ready + Push) @@ -45,18 +56,20 @@ jobs: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` -### 2. Add the secret +The `uses:` line is the only thing that points back here. Everything else β€” prompt loading, checkout, Claude invocation β€” runs inside the reusable workflow in this repo. + +### 3. Add the secret to each target repo -In your target repo: **Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret** +**Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret** - Name: `ANTHROPIC_API_KEY` - Value: your Anthropic API key -### 3. Pin to a release tag +That's it. Future updates to the prompt or workflow logic only need to be made here; target repos pick them up automatically on the next tagged release. -Reference `@v1` (or a specific tag) to pin to a stable version. Check the [releases](../../releases) page for available tags. +--- -## Customizing the prompt +## Customizing the prompt per repo To override the default Codex prompt for a specific repo, pass `prompt_override`: @@ -86,8 +99,11 @@ Concurrency is keyed on `github.repository + PR number` so rapid pushes cancel t ## Versioning -- `v1` β€” initial release -- Tag releases with `git tag v1 && git push origin v1` +| Tag | Notes | +|---|---| +| `v1` | Initial release | + +Breaking changes β†’ new major tag (`v2`). Non-breaking improvements β†’ minor tag (`v1.1`). ## Permissions From 080ab2cf9915a97694b7b5050d49adf53fd30c7c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 19:43:51 +0000 Subject: [PATCH 3/4] Add context files agent, MkDocs docs, and repo AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context files agent: - prompts/context_files_prompt.md β€” verbatim prompt for generating / updating AGENTS.md and CLAUDE.md; uses $ARGUMENTS substitution for mode (auto/quick-start/update/evaluate) and scope - .github/workflows/context_files_agent.yml β€” reusable workflow_call; accepts agent_args (mode+scope) and prompt_override; needs contents: write to commit context files back to PR branches - .github/workflows/context_files_pr.yml β€” caller workflow triggered on pull_request opened + ready_for_review MkDocs documentation site: - mkdocs.yml β€” Material theme, nav covering all pages - .github/workflows/docs.yml β€” build-check on PRs, gh-deploy on main - docs/index.md β€” overview and architecture diagram - docs/user-stories.md β€” solo dev, team lead, DevEx engineer stories - docs/using.md β€” step-by-step setup, trigger table, prompt override guide - docs/contributing.md β€” three-file pattern for adding a new agent - docs/agents/index.md β€” agent catalog and shared design principles - docs/agents/pr-review.md β€” review agent triggers, caller snippet, prompt - docs/agents/context-files.md β€” context agent modes, agent_args, permissions Repo context files (generated via context files prompt logic): - AGENTS.md β€” WHY/WHAT/HOW, repo map, validated commands, gotchas - CLAUDE.md β€” symlink to AGENTS.md (Claude Code auto-discovery) - .gitignore β€” remove CLAUDE.md exclusion so symlink is tracked https://claude.ai/code/session_018moumKtzZQiSiyGxVUFGdS --- .github/workflows/context_files_agent.yml | 64 ++++ .github/workflows/context_files_pr.yml | 21 ++ .github/workflows/docs.yml | 38 ++ .gitignore | 1 - AGENTS.md | 107 ++++++ CLAUDE.md | 1 + docs/agents/context-files.md | 104 ++++++ docs/agents/index.md | 32 ++ docs/agents/pr-review.md | 91 +++++ docs/contributing.md | 150 ++++++++ docs/index.md | 40 +++ docs/user-stories.md | 60 ++++ docs/using.md | 166 +++++++++ mkdocs.yml | 53 +++ prompts/context_files_prompt.md | 408 ++++++++++++++++++++++ 15 files changed, 1335 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/context_files_agent.yml create mode 100644 .github/workflows/context_files_pr.yml create mode 100644 .github/workflows/docs.yml create mode 100644 AGENTS.md create mode 120000 CLAUDE.md create mode 100644 docs/agents/context-files.md create mode 100644 docs/agents/index.md create mode 100644 docs/agents/pr-review.md create mode 100644 docs/contributing.md create mode 100644 docs/index.md create mode 100644 docs/user-stories.md create mode 100644 docs/using.md create mode 100644 mkdocs.yml create mode 100644 prompts/context_files_prompt.md diff --git a/.github/workflows/context_files_agent.yml b/.github/workflows/context_files_agent.yml new file mode 100644 index 0000000..9d70030 --- /dev/null +++ b/.github/workflows/context_files_agent.yml @@ -0,0 +1,64 @@ +name: Context files agent (AGENTS.md / CLAUDE.md) + +on: + workflow_call: + inputs: + agent_args: + description: > + Arguments passed to the context files prompt (mode + scope). + Defaults to "auto ." which runs in auto mode on the repo root. + type: string + required: false + default: "auto ." + prompt_override: + description: > + Full prompt text to use instead of the default context files prompt. + Overrides agent_args when set. + type: string + required: false + default: "" + secrets: + ANTHROPIC_API_KEY: + required: true + +jobs: + context-files: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + + steps: + # Checkout the *target repository* so Claude can read the codebase. + - name: Checkout target repo + uses: actions/checkout@v4 + + # Checkout this workflow repo to read the prompt template. + - name: Checkout workflow repo (for prompt) + uses: actions/checkout@v4 + with: + repository: safurrier/python-collab-template + path: _ai_workflows + + - name: Load prompt + id: prompt + shell: bash + run: | + if [ -n "${{ inputs.prompt_override }}" ]; then + echo "text<> "$GITHUB_OUTPUT" + printf "%s\n" "${{ inputs.prompt_override }}" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + else + ARGS="${{ inputs.agent_args }}" + echo "text<> "$GITHUB_OUTPUT" + sed "s|\$ARGUMENTS|${ARGS}|g" \ + _ai_workflows/prompts/context_files_prompt.md >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Run context files agent + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: ${{ steps.prompt.outputs.text }} diff --git a/.github/workflows/context_files_pr.yml b/.github/workflows/context_files_pr.yml new file mode 100644 index 0000000..65298f8 --- /dev/null +++ b/.github/workflows/context_files_pr.yml @@ -0,0 +1,21 @@ +name: Context files agent (PR opened / Draft->Ready) + +on: + pull_request: + types: [opened, ready_for_review] + +concurrency: + group: context-files-agent-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + context-files: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + # Optional: pass explicit mode/scope or a full prompt override: + # with: + # agent_args: "auto src/" + # prompt_override: | + # diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f8f71a2 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,38 @@ +name: Documentation + +on: + push: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + pull_request: + branches: [main] + paths: + - "docs/**" + - "mkdocs.yml" + +jobs: + deploy: + if: github.event_name == 'push' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force + + build-check: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install mkdocs-material + - run: mkdocs build --strict diff --git a/.gitignore b/.gitignore index d514899..780bfaf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .idea/ .vs_code/* .aider* -CLAUDE.md # GitHub Actions local runner (act) .actrc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5a0383a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,107 @@ +# AGENTS.md + +## WHY β€” What this repo is + +Central GitHub Actions workflow host for **Claude PR agents** β€” reusable, prompt-driven automation that attaches to pull requests. Instead of duplicating workflow logic across repos, any target repo references this one with a single `uses:` line and gets automated code review, context file generation, or any other Claude-powered task. + +The repo is intentionally minimal: workflow YAML, prompt files, and documentation. No build system, no runtime dependencies. + +--- + +## WHAT β€” Repo map + +``` +.github/workflows/ + claude_pr_review.yml # Reusable: code review agent (workflow_call) + context_files_agent.yml # Reusable: AGENTS.md / CLAUDE.md generator (workflow_call) + ai_pr_review.yml # Caller example: code review for this repo + context_files_pr.yml # Caller example: context files agent for this repo + docs.yml # MkDocs build + GitHub Pages deploy + +prompts/ + codex_code_review_prompt.md # Verbatim Codex Code Review prompt (OpenAI cookbook) + context_files_prompt.md # Context files generator/updater prompt + +docs/ # MkDocs documentation site source + index.md # Overview and architecture + user-stories.md # Who uses this and why + using.md # Step-by-step setup for target repos + contributing.md # How to add a new agent + agents/ + index.md # Agent catalog + pr-review.md # Code review agent docs + context-files.md # Context files agent docs +``` + +--- + +## HOW β€” How to work here + +### Adding a new agent + +1. Create `prompts/.md` β€” the Claude instruction text +2. Create `.github/workflows/.yml` β€” copy an existing reusable workflow, swap the prompt filename, adjust permissions if the agent writes files +3. Create `docs/agents/.md` β€” trigger table, caller snippet, customization options +4. Add entry to `docs/agents/index.md` and `mkdocs.yml` nav +5. Tag a new release: `git tag vX.Y && git push origin vX.Y` + +See `docs/contributing.md` for the full checklist. + +### Updating a prompt + +Edit the file in `prompts/`. Bump the version tag so target repos can opt in to the updated prompt on their own schedule. + +### Working on docs + +```bash +# Preview locally (requires mkdocs-material) +pip install mkdocs-material +mkdocs serve +``` +⏸️ Not executed β€” requires network install. Confirm `mkdocs.yml` exists: βœ… + +```bash +# Strict build (catches broken links, missing pages) +mkdocs build --strict +``` +⏸️ Not executed β€” same prerequisite. + +### Validating workflow YAML + +```bash +yamllint .github/workflows/ +``` +⏸️ Not executed β€” `yamllint` not confirmed in this environment. Workflows are validated by GitHub Actions on push. + +--- + +## Common commands (validated) + +| Command | Status | Notes | +|---|---|---| +| `mkdocs serve` | ⏸️ not executed | Requires `pip install mkdocs-material`; run from repo root | +| `mkdocs build --strict` | ⏸️ not executed | Same prerequisite; used in CI (`docs.yml`) | +| `git tag vX.Y && git push origin vX.Y` | ⏸️ not executed | Release process; target repos pin to these tags | + +No test suite, linter, or build step for the workflows themselves β€” validation happens when GitHub Actions parses them on push. + +--- + +## Progressive disclosure + +- `docs/using.md` β€” full setup walkthrough for target repos +- `docs/contributing.md` β€” step-by-step agent contribution guide +- `docs/agents/pr-review.md` β€” code review agent: triggers, prompt override, permissions +- `docs/agents/context-files.md` β€” context files agent: modes, `agent_args`, permissions +- `.github/workflows/claude_pr_review.yml` β€” canonical source for reusable workflow structure +- `prompts/context_files_prompt.md` β€” canonical source for context files prompt spec + +--- + +## Gotchas + +- **Repo rename breaks callers**: the reusable workflows hard-code `repository: safurrier/python-collab-template` to check out the prompt files. If this repo is renamed, update that field in every reusable workflow. +- **Private repo access**: if this repo is private, target repos must be granted access via Settings β†’ Actions β†’ Access β†’ "Accessible from repositories in your account". +- **Tag before using**: target repos reference `@v1` (or another tag). Push a tag before pointing any repo at this one. +- **`contents: write` scope**: the context files agent needs `contents: write` to commit `AGENTS.md` / `CLAUDE.md`. The review agent only needs `read`. +- **`$ARGUMENTS` substitution**: the context files prompt uses a `$ARGUMENTS` placeholder that the workflow substitutes via `sed` at runtime. Do not treat it as a shell variable in the prompt file itself. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/docs/agents/context-files.md b/docs/agents/context-files.md new file mode 100644 index 0000000..02cbb35 --- /dev/null +++ b/docs/agents/context-files.md @@ -0,0 +1,104 @@ +# Context Files Agent + +Keeps `AGENTS.md` and `CLAUDE.md` up to date in a repository. When these files are missing, Claude generates them from scratch. When they exist, Claude audits and updates them to reflect the current codebase. + +## Files + +| File | Path | +|---|---| +| Reusable workflow | `.github/workflows/context_files_agent.yml` | +| Prompt | `prompts/context_files_prompt.md` | + +## What it does + +Claude reads the repo structure, config files, CI workflows, and any existing context docs, then: + +1. **If no context files exist** (`auto` β†’ `quick-start` mode): generates `AGENTS.md` with a WHY/WHAT/HOW structure and creates `CLAUDE.md` as a symlink +2. **If files already exist** (`auto` β†’ `update + evaluate` mode): validates commands, removes stale content, improves conciseness, and creates nested `AGENTS.md` files for subdirectories with distinct workflows +3. **Commits the result** directly to the PR branch so the updated context files are part of the PR + +The prompt follows a strict principle: **validate what you write**. Every command in the generated docs is confirmed to exist in Makefiles, CI configs, or package manifests before being included. + +## Output files + +| File | Role | +|---|---| +| `AGENTS.md` | Source of truth for agent onboarding β€” WHY/WHAT/HOW, validated commands, progressive disclosure pointers | +| `CLAUDE.md` | Symlink to `AGENTS.md` (Claude Code auto-discovers `CLAUDE.md` files when navigating a repo) | +| `ai_agent_docs/*.md` | Cross-cutting topic docs (architecture, conventions, etc.) created when content warrants it | +| `/AGENTS.md` | Nested docs for subdirectories with their own distinct tooling | + +## Triggers + +| Event | Runs? | +|---|---| +| PR first opened (non-draft) | Yes | +| PR marked Ready for review | Yes | +| New commits pushed to open PR | No (use review agent for that) | +| Draft PR | No | + +## Minimal caller workflow + +```yaml +name: Context files agent + +on: + pull_request: + types: [opened, ready_for_review] + +concurrency: + group: context-files-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + context-files: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +## Customizing scope or mode + +Use `agent_args` to change which directory Claude targets or which mode it runs in: + +```yaml +with: + agent_args: "auto src/" # scope to src/ only + # agent_args: "quick-start ." # always generate fresh, never update + # agent_args: "evaluate ." # audit and propose improvements, report in chat +``` + +Modes: + +| Mode | Behavior | +|---|---| +| `auto` (default) | Generate if missing; update + evaluate if present. No confirmation prompts. | +| `quick-start` | Always generate from scratch. Asks before overwriting. | +| `update` | Update existing files only. | +| `evaluate` | Audit files against quality principles and propose improvements. | + +## Full prompt override + +To replace the entire prompt (e.g., for a simpler or domain-specific context file format): + +```yaml +with: + prompt_override: | + Look at this repo and create AGENTS.md with: + 1. A one-sentence description of what the repo does + 2. How to run tests + 3. How to run the linter + Keep it under 30 lines. +``` + +## Permissions + +```yaml +permissions: + contents: write # needed to commit AGENTS.md / CLAUDE.md to the branch + pull-requests: write + issues: write +``` + +Unlike the review agent, this one needs `contents: write` because it commits files. diff --git a/docs/agents/index.md b/docs/agents/index.md new file mode 100644 index 0000000..7fa4a26 --- /dev/null +++ b/docs/agents/index.md @@ -0,0 +1,32 @@ +# Agents + +Each agent is a self-contained pair of files: + +| File | Purpose | +|---|---| +| `prompts/.md` | The instruction text sent to Claude | +| `.github/workflows/.yml` | The reusable `workflow_call` that loads the prompt and runs the action | + +Target repos reference the reusable workflow with a `uses:` line and supply `ANTHROPIC_API_KEY`. The prompt loading, checkout, and Claude invocation all happen inside the reusable workflow here. + +--- + +## Available agents + +| Agent | Reusable workflow | Prompt file | +|---|---|---| +| [PR Code Review](pr-review.md) | `claude_pr_review.yml` | `codex_code_review_prompt.md` | +| [Context Files](context-files.md) | `context_files_agent.yml` | `context_files_prompt.md` | + +--- + +## Shared design principles + +**Every agent:** + +- Accepts a `prompt_override` input so any repo can swap in custom instructions without forking +- Uses the same checkout pattern: target repo first, then this workflow repo for the prompt +- Runs on `ubuntu-latest` with least-privilege permissions (only escalating `contents` to `write` when the agent needs to commit files) +- Can be called independently β€” agents don't depend on each other + +**Prompt files** live in `prompts/` and are loaded at runtime, not embedded in the workflow YAML. This means you can update a prompt and tag a new release without touching any workflow logic. diff --git a/docs/agents/pr-review.md b/docs/agents/pr-review.md new file mode 100644 index 0000000..e63b5de --- /dev/null +++ b/docs/agents/pr-review.md @@ -0,0 +1,91 @@ +# PR Code Review Agent + +Reviews PR diffs using the official [Codex Code Review prompt](https://developers.openai.com/cookbook/examples/codex/build_code_review_with_codex_sdk/) and posts findings directly to the PR. + +## Files + +| File | Path | +|---|---| +| Reusable workflow | `.github/workflows/claude_pr_review.yml` | +| Prompt | `prompts/codex_code_review_prompt.md` | + +## What it does + +Claude reads the PR diff and flags issues in these categories: + +- **Correctness** β€” logic errors, wrong assumptions, broken edge cases +- **Performance** β€” algorithmic problems, unnecessary work +- **Security** β€” OWASP top-10 class issues, injection risks, credential exposure +- **Maintainability** β€” API misuse, dead code, structural problems +- **Developer experience** β€” confusing interfaces, missing context + +It then produces an overall verdict β€” `patch is correct` or `patch is incorrect` β€” with a confidence score between 0 and 1. + +## Triggers + +| Event | Runs? | +|---|---| +| Draft PR opened or pushed | No | +| PR marked Ready for review | Yes | +| New commits pushed to open PR | Yes | +| PR reopened (non-draft) | Yes | + +## Minimal caller workflow + +```yaml +name: Claude PR scan + +on: + pull_request: + types: [ready_for_review, synchronize, reopened] + +concurrency: + group: claude-pr-scan-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + review: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +## Customizing the prompt + +The `prompt_override` input replaces the entire default prompt. This makes the workflow a generic "run Claude on a PR with any prompt" mechanism β€” the code review prompt just happens to be the default. + +```yaml +jobs: + review: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + prompt_override: | + Review this diff for any violation of our REST API conventions: + - All endpoints must use snake_case paths + - Response envelopes must include a top-level "data" key + - Error responses must include "code" and "message" fields + Flag each violation with file and line number. Give a pass/fail verdict. +``` + +Because the workflow is generic, you can use `prompt_override` to turn this into any kind of PR agent β€” a changelog enforcer, a migration validator, a documentation checker β€” without adding a new workflow. + +## Permissions + +```yaml +permissions: + contents: read + pull-requests: write + issues: write +``` + +`pull-requests: write` and `issues: write` are needed for Claude to post comments to the PR. + +## Default prompt + +The prompt is the verbatim text from the OpenAI Codex cookbook. Source: [prompts/codex_code_review_prompt.md](https://github.com/safurrier/python-collab-template/blob/main/prompts/codex_code_review_prompt.md) + +> You are acting as a reviewer for a proposed code change made by another engineer. Focus on issues that impact correctness, performance, security, maintainability, or developer experience. Flag only actionable issues introduced by the pull request. When you flag an issue, provide a short, direct explanation and cite the affected file and line range. Prioritize severe issues and avoid nit-level comments unless they block understanding of the diff. After listing findings, produce an overall correctness verdict ("patch is correct" or "patch is incorrect") with a concise justification and a confidence score between 0 and 1. Ensure that file citations and line numbers are exactly correct using the tools available; if they are incorrect your comments will be rejected. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..f34bacf --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,150 @@ +# Contributing a New Agent + +Adding an agent takes three files and a tag bump. No framework, no registration step. + +## The pattern + +Every agent is: + +1. A **prompt file** in `prompts/` β€” the instruction text Claude receives +2. A **reusable workflow** in `.github/workflows/` β€” the Actions plumbing +3. A **doc page** in `docs/agents/` β€” how to use it and what it does + +That's it. The reusable workflow is almost identical for every agent; you're mostly just changing the prompt file name and adjusting permissions if the agent needs to write files. + +--- + +## Step 1 β€” Write the prompt + +Create `prompts/.md`. + +Write the prompt as you'd write a Claude system prompt or slash-command definition. A few guidelines: + +- Be specific about the **output format** (what Claude should post, commit, or return) +- Describe the **scope** (what Claude should read β€” diff only, full repo, specific files) +- If the prompt is parameterizable (like context files' mode/scope), use a `$ARGUMENTS` placeholder and document the substitution in the workflow +- Keep it focused β€” one agent, one job + +--- + +## Step 2 β€” Create the reusable workflow + +Create `.github/workflows/.yml`. + +Use the existing agents as templates. The structure is always: + +```yaml +name: + +on: + workflow_call: + inputs: + prompt_override: + type: string + required: false + default: "" + # Add any agent-specific inputs here (e.g., agent_args) + secrets: + ANTHROPIC_API_KEY: + required: true + +jobs: + : + runs-on: ubuntu-latest + permissions: + contents: read # bump to write if the agent commits files + pull-requests: write + issues: write + + steps: + - name: Checkout target repo + uses: actions/checkout@v4 + + - name: Checkout workflow repo (for prompt) + uses: actions/checkout@v4 + with: + repository: safurrier/python-collab-template + path: _ai_workflows + + - name: Load prompt + id: prompt + shell: bash + run: | + if [ -n "${{ inputs.prompt_override }}" ]; then + echo "text<> "$GITHUB_OUTPUT" + printf "%s\n" "${{ inputs.prompt_override }}" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + else + echo "text<> "$GITHUB_OUTPUT" + cat _ai_workflows/prompts/.md >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + fi + + - name: Run + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: ${{ steps.prompt.outputs.text }} +``` + +**Key decisions:** + +- `contents: read` vs `contents: write` β€” use `write` only if the agent commits files back to the branch +- Add extra inputs (like `agent_args`) if the prompt is parameterizable +- Use `sed "s|\$ARGUMENTS|${ARGS}|g"` in the Load prompt step when substituting arguments into the prompt + +--- + +## Step 3 β€” Write the doc page + +Create `docs/agents/.md`. Include: + +- What it does (1–2 sentences) +- The files table (reusable workflow + prompt path) +- Trigger event table +- Minimal caller workflow (copy-paste ready) +- Customization options (`prompt_override`, any extra inputs) +- Permissions block + +Then add it to the nav in `mkdocs.yml`: + +```yaml +nav: + - Agents: + - Overview: agents/index.md + - PR Code Review: agents/pr-review.md + - Context Files: agents/context-files.md + - Your Agent: agents/your-agent-name.md # add this line +``` + +And add a row to the table in `docs/agents/index.md`. + +--- + +## Step 4 β€” Tag a new release + +```bash +git add prompts/.md \ + .github/workflows/.yml \ + docs/agents/.md \ + docs/agents/index.md \ + mkdocs.yml +git commit -m "feat: add agent" +git tag v1.1 # or next semver +git push origin main --tags +``` + +Target repos that want the new agent update their caller workflow to reference `@v1.1`. + +--- + +## Checklist + +- [ ] `prompts/.md` β€” prompt written and reviewed +- [ ] `.github/workflows/.yml` β€” reusable workflow using `workflow_call` +- [ ] `prompt_override` input present with `default: ""` +- [ ] Permissions set to minimum required (`contents: write` only if needed) +- [ ] `docs/agents/.md` β€” doc page with trigger table and caller snippet +- [ ] Row added to `docs/agents/index.md` +- [ ] Entry added to `mkdocs.yml` nav +- [ ] New semver tag pushed diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..4f5d6ce --- /dev/null +++ b/docs/index.md @@ -0,0 +1,40 @@ +# Claude PR Agents + +A collection of reusable GitHub Actions workflows that attach Claude-powered automation to pull requests. Pick an agent, drop a 15-line caller workflow into your repo, add an API key, and every PR gets automated review, context file generation, or any other prompt-driven task. + +## Available agents + +| Agent | Trigger | What it does | +|---|---|---| +| [PR Code Review](agents/pr-review.md) | Draftβ†’Ready, new commits pushed | Reviews the diff; posts findings and a correctness verdict | +| [Context Files](agents/context-files.md) | PR opened, Draftβ†’Ready | Creates or updates `AGENTS.md` / `CLAUDE.md` so future agents are oriented | + +## How it works + +This repo is the **central workflow host**. Each agent lives in two files: + +- A **prompt** (`prompts/.md`) β€” the instruction text sent to Claude +- A **reusable workflow** (`.github/workflows/.yml`) β€” the Actions plumbing + +Target repos only need a thin caller workflow: + +``` +Your repo This repo (central host) +───────────────────────────────────── ──────────────────────────────────────── +.github/workflows/ai_pr_review.yml β†’ .github/workflows/claude_pr_review.yml + uses: safurrier/...@v1 β”œβ”€β”€ checks out your repo + secrets: ANTHROPIC_API_KEY β”œβ”€β”€ loads prompts/codex_code_review_prompt.md + └── runs anthropics/claude-code-action@v1 +``` + +When a PR event fires, GitHub fetches the reusable workflow from this repo, runs it in your repo's context, and Claude posts its output back to the PR thread. + +## Quick start + +β†’ [Using in Your Repo](using.md) β€” step-by-step setup guide + +## Add a new agent + +Anyone can contribute a new agent by adding a prompt file, a reusable workflow, and a doc page. + +β†’ [Contributing](contributing.md) diff --git a/docs/user-stories.md b/docs/user-stories.md new file mode 100644 index 0000000..9a95a4b --- /dev/null +++ b/docs/user-stories.md @@ -0,0 +1,60 @@ +# User Stories + +## Solo developer maintaining multiple repos + +**As a developer who maintains several personal or side-project repositories,** +I want a code review pass to run automatically whenever I mark a PR ready, +so that I catch correctness issues and security problems before merging without having to remember to trigger it manually. + +**As a solo developer working quickly,** +I want the review to re-run when I push a follow-up commit to address feedback, +so that I get a fresh verdict on the updated code without creating a new PR. + +**As a solo developer who forgets to write agent context files,** +I want a workflow that notices when `AGENTS.md` / `CLAUDE.md` are missing and generates them automatically when I open a PR, +so that future AI-assisted sessions in that repo are pre-oriented without extra effort. + +--- + +## Team lead or engineering manager + +**As a team lead responsible for code quality across multiple repos,** +I want a consistent, documented review standard applied to every PR across all our repos, +so that automated reviews use the same criteria regardless of who's reviewing. + +**As a team lead,** +I want to be able to override the review prompt per-repo when one codebase has unique concerns (e.g., a security-sensitive service or a performance-critical library), +so that the review is targeted without having to fork the workflow logic. + +**As a team lead,** +I want to pin to a specific release tag of the central workflow repo, +so that I control when my team picks up changes and can test upgrades in a staging repo first. + +--- + +## Platform / DevEx engineer + +**As a DevEx engineer building internal tooling,** +I want a single central repo of reusable workflows that any team can reference with one `uses:` line, +so that maintaining or updating the review logic is a one-place change rather than a PR to every repo. + +**As a platform engineer,** +I want to contribute a new agent (e.g., a dependency audit agent, a documentation coverage agent) by following a clear pattern, +so that onboarding new automated tasks is predictable and doesn't require understanding a complex framework. + +**As a platform engineer,** +I want each agent to be independently callable, +so that repos can mix and match agents without coupling. + +--- + +## What these stories drive + +| Story theme | Feature | +|---|---| +| Automatic, no-remember trigger | `ready_for_review` + `synchronize` events; draft guard | +| No double-run on rapid pushes | Concurrency with `cancel-in-progress: true` | +| Per-repo prompt customization | `prompt_override` input on every reusable workflow | +| Version pinning | Semver tags (`@v1`, `@v1.1`) on this repo | +| Extend without duplication | New agent = prompt file + reusable workflow + doc page | +| Context files on autopilot | `context_files_agent.yml` on `opened` + `ready_for_review` | diff --git a/docs/using.md b/docs/using.md new file mode 100644 index 0000000..76dc268 --- /dev/null +++ b/docs/using.md @@ -0,0 +1,166 @@ +# Using in Your Repo + +Each agent is independent. Add only the ones you want. + +## Prerequisites + +- An Anthropic API key ([get one here](https://console.anthropic.com/)) +- Write access to your target repo +- This central repo tagged at `v1` (one-time, see below) + +--- + +## One-time: tag this repo + +Before any target repo can call a reusable workflow, you need a stable tag to pin to. + +```bash +git tag v1 +git push origin v1 +``` + +Future releases follow the same pattern: `v1.1`, `v2`, etc. Target repos stay pinned to their chosen tag until you update the `uses:` line. + +--- + +## Add an agent to a repo + +### Step 1 β€” Create the caller workflow file + +Create `.github/workflows/ai_pr_review.yml` in your target repo with the agents you want. Each agent is a separate job. + +**Code review only:** + +```yaml +name: Claude PR scan + +on: + pull_request: + types: [ready_for_review, synchronize, reopened] + +concurrency: + group: claude-pr-scan-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + review: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +**Context files only:** + +```yaml +name: Context files agent + +on: + pull_request: + types: [opened, ready_for_review] + +concurrency: + group: context-files-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + context-files: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +**Both agents together** β€” put them in one file as separate jobs: + +```yaml +name: Claude PR agents + +on: + pull_request: + types: [opened, ready_for_review, synchronize, reopened] + +concurrency: + group: claude-agents-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + review: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + context-files: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` + +### Step 2 β€” Add the secret + +In your target repo: **Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret** + +| Field | Value | +|---|---| +| Name | `ANTHROPIC_API_KEY` | +| Value | Your Anthropic API key | + +### Step 3 β€” Open a PR and verify + +Open a draft PR, then mark it **Ready for review**. The workflow should appear in the **Actions** tab and Claude should post output to the PR within a minute or two. + +--- + +## Customizing a prompt + +Every agent accepts a `prompt_override` input that replaces the default prompt entirely. Use this when you want different review criteria for a specific repo. + +```yaml +jobs: + review: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + prompt_override: | + You are reviewing a security-sensitive payments service. + Flag any use of eval(), exec(), unsanitized SQL, or raw HTTP calls. + Produce a pass/fail verdict with severity ratings. +``` + +The context files agent additionally accepts `agent_args` to control mode and scope without needing a full override: + +```yaml +jobs: + context-files: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + with: + agent_args: "auto src/" # scope to src/ instead of repo root +``` + +--- + +## Trigger policy reference + +| Event | Review agent | Context files agent | +|---|---|---| +| PR opened as draft | Skipped | Skipped | +| Draft PR gets new commits | Skipped | Skipped | +| PR marked Ready for review | Runs | Runs | +| New commits pushed to open PR | Runs | Not triggered | +| Closed PR reopened | Runs (if not draft) | Not triggered | +| PR first opened (non-draft) | Not triggered | Runs | + +--- + +## Private vs public repos + +Use `pull_request` (not `pull_request_target`) β€” this is the correct event for repos without forks. The `ANTHROPIC_API_KEY` secret is accessible to `pull_request` workflows run from branches in the same repo. + +If your central workflow repo (this one) is **private**, target repos must be granted access: **Settings β†’ Actions β†’ Access β†’ Accessible from repositories in your account**. diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..aa3d27b --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,53 @@ +site_name: Claude PR Agents +site_description: Reusable GitHub Actions workflows for AI-powered PR automation using Claude +site_url: https://safurrier.github.io/python-collab-template/ +repo_url: https://github.com/safurrier/python-collab-template +repo_name: safurrier/python-collab-template +edit_uri: edit/main/docs/ + +theme: + name: material + palette: + - scheme: default + primary: indigo + accent: indigo + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - scheme: slate + primary: indigo + accent: indigo + toggle: + icon: material/brightness-4 + name: Switch to light mode + features: + - navigation.tabs + - navigation.sections + - navigation.top + - content.code.copy + - content.code.annotate + - toc.follow + +nav: + - Home: index.md + - User Stories: user-stories.md + - Using in Your Repo: using.md + - Agents: + - Overview: agents/index.md + - PR Code Review: agents/pr-review.md + - Context Files: agents/context-files.md + - Contributing: contributing.md + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + - admonition + - pymdownx.details + - attr_list + - md_in_html + - tables + - toc: + permalink: true diff --git a/prompts/context_files_prompt.md b/prompts/context_files_prompt.md new file mode 100644 index 0000000..4bac298 --- /dev/null +++ b/prompts/context_files_prompt.md @@ -0,0 +1,408 @@ +--- +name: ai-agent-context-files +description: Create, update, or evaluate AGENTS.md and CLAUDE.md files for a repository. This skill activates when users want to create onboarding docs, update agent context files, or evaluate existing documentation quality. Follows WHY/WHAT/HOW structure with validated commands and progressive disclosure. +--- + +You are operating inside a real codebase. Your job is to produce and maintain **high-signal agent onboarding memory files**: + +* `AGENTS.md` (cross-tool, open format) +* `CLAUDE.md` (Claude Code context file) + +You must follow these principles: + +* **Stateless onboarding**: assume you know nothing about this repo until you read it; the files you write should onboard future agent sessions. +* **Less is more**: keep contents concise and universally applicable. Target **<150 lines** for a root file when possible; **hard stop at ~300 lines** unless you have a specific, justified reason. +* **WHY / WHAT / HOW**: + + * WHY: what this repo is for + * WHAT: map of the repo (where things live) + * HOW: how to work here (commands + validation + workflows) +* **Progressive disclosure**: do not stuff everything into AGENTS/CLAUDE. Prefer links/pointers to authoritative docs/config/scripts inside the repo. **Actively create nested AGENTS.md** for module-specific docs (Claude Code auto-discovers these). **Actively create ai_agent_docs/** for cross-cutting concerns that span modules. Both can exist at any scope level. Lean toward creating these files rather than cramming content into the root fileβ€”they keep individual files lean and focused. Each nested file should be small and self-contained. +* **Claude is not a linter**: do not write verbose style guides. Prefer deterministic tools (formatters/linters/typecheckers) and tell how to run them. +* **Validate anything you claim**: + + * If you include a command, validate it exists (scripts/Makefile/Taskfile/CI docs/etc). + * Treat install/bootstrap commands as potentially networked and stateful; validate by confirming scripts/targets exist and prefer --version / --help over executing installs unless the user explicitly asks + * Prefer running a cheap validation (`--help`, list scripts, dry-run) and record whether you executed it. + * If you cannot execute a command in this environment, mark it as **"not executed"** and explain what prevented it. + +Security/ops guardrails: + +* Never write secrets (API keys, tokens, connection strings) into AGENTS/CLAUDE. +* Do not run destructive shell commands. +* Prefer offline, unit-level validation; avoid networked/E2E/integration/data-mutating commands unless the user explicitly asks. +* If you detect security-sensitive instructions in existing files, propose safer rewrites. + +--- + +## Inputs (from slash command arguments) + +RAW ARGUMENTS: +`$ARGUMENTS` + +### Help mode (special) + +If `$ARGUMENTS` contains `--help` anywhere (or is exactly `help`), do **not** modify the repo. + +Instead, print a compact usage guide: + +* What the command does +* Modes and what they mean +* Target flags +* How `scope` works +* 6–10 common examples + +Default assumption when not using `--help`: mode defaults to `auto`. + +### Parsing rules + + +Parse `$ARGUMENTS` by: + 1) Extract known flags anywhere: --help, --agents, --claude, --both + 2) Remove them from the token list + 3) Parse remaining tokens as: [mode] [scope...] + - scope can be one or more paths (space or comma separated) + 4) Defaults: mode=auto, scope="." + * `--both` (default) + * `--agents` (only AGENTS.md) + * `--claude` (only CLAUDE.md) + +If mode is omitted, assume `auto`. + +**Multi-scope behavior**: +- When multiple scopes provided: create/update AGENTS.md in each scope +- Root AGENTS.md: update to reference nested docs (if it exists) +- Each scope is processed independently following the same rules + +**Examples**: +``` +/agent-docs auto config/nvim config/zsh config/ai-config +/agent-docs auto config/nvim, config/zsh, config/ai-config +``` + +Mode behaviors: + +* **quick-start** + + * Create missing file(s) from scratch. + * If scope is ambiguous (monorepo, multiple apps, unclear root), ask the user with 2–3 best-guess options. + * **CLAUDE.md consolidation prompt**: If CLAUDE.md exists as a standalone file (not a symlink), ask the user if they want to consolidate it into AGENTS.md and replace CLAUDE.md with a symlink. +* **update** + + * If file(s) exist, update them to reflect current reality. + * Validate that commands/paths referenced are still valid; fix or propose changes when stale. + * **CLAUDE.md consolidation prompt**: If CLAUDE.md exists as a standalone file (not a symlink), ask the user if they want to consolidate it into AGENTS.md and replace CLAUDE.md with a symlink. +* **evaluate** + + * Do everything in update mode, plus: + * Evaluate file(s) against the principles above; propose improvements (and apply safe improvements when appropriate). + * **CLAUDE.md consolidation prompt**: If CLAUDE.md exists as a standalone file (not a symlink), ask the user if they want to consolidate it into AGENTS.md and replace CLAUDE.md with a symlink. +* **auto** + + * If neither AGENTS.md nor CLAUDE.md exists in the chosen scope: behave like quick-start. + * Otherwise: behave like update + evaluate. + * **Do not ask the user for permission/confirmation.** Make best-judgment changes and then report exactly what changed and why. + * **CLAUDE.md consolidation (auto only)**: If CLAUDE.md exists as a standalone file (not a symlink to AGENTS.md), automatically consolidate its content into AGENTS.md and replace CLAUDE.md with a symlink to AGENTS.md. Report what content was merged. + * **Nested docs creation (auto mode default behavior)**: + - **Always** scan for directories that have their own workflows, commands, or tools β€” create nested `AGENTS.md` + `CLAUDE.md` symlink in each + - **Always** identify cross-cutting topics (architecture, conventions, testing, deployment) β€” create `ai_agent_docs/` files for them + - When multiple scopes provided: create nested `AGENTS.md` in each scope + - Reference all nested docs in the root AGENTS.md + - Prefer more smaller files over fewer bloated ones; each file stays lean and focused + +--- + +## Execution plan (follow this sequence) + +### 1) Determine root and scope precisely + +* If this is a git repo, find the git root (preferred anchor). If not, treat the provided scope directory as the anchor. +* Interpret `scope` as the **target sub-area** when provided (e.g., `services/payments`, `packages/foo`, `src/moduleX`). +* Detect monorepo signals (workspaces, multiple services/apps, many package manifests). +* Decide whether the user likely intends: + + * root-only onboarding docs, or + * root + subproject-specific docs (nested instructions) + +**Scope selection rules:** + +* If the user provided a `scope` path, anchor your work there. + + * Prefer writing onboarding docs at that scope's project root (e.g., the nearest directory containing a package/build manifest, or the nearest meaningful boundary like `services//`, `packages//`). +* quick-start/update/evaluate: if multiple reasonable anchors exist for the given scope, ask the user to pick (2–3 best-guess options). +* auto: pick the best default without asking: + + 1. If `scope` is provided: write docs for that scope anchor. + 2. Else write a root file at the git root. + 3. Proactively create nested AGENTS.md files for directories that have their own workflows, commands, or configuration. Also create `ai_agent_docs/` for cross-cutting topics. Prefer more smaller files over fewer bloated ones. + +**Optional inference when `scope` is omitted (best effort, read-only):** + +* If available, use cheap git signals to infer the most relevant sub-area: + + * `git status --porcelain` / `git diff --name-only` to see currently-changed files. + * `git log -n 20 --name-only` to see recently-touched paths (if fast). + * If one directory dominates the touched paths, treat that as an implied scope anchor. +* If signals are absent/ambiguous, fall back to git root. + +When you infer scope this way, state explicitly what signal you used and what you inferred. + +**Scope guardrail:** + +* **Respect explicit scope**: When a user provides a specific path (e.g., "create AGENTS.md for discord_api/"), only create docs within that scope. Do not create docs in parent directories, sibling directories, or the repo root unless the user asks. + +**Context management principles** (use judgment, not hard limits): + +* Every file should earn its existence β€” if it would be near-empty or just repeat a parent, fold it in +* Deeper nesting should mean more specific content, not structural boilerplate +* `ai_agent_docs/` files should cover topics that genuinely span multiple modules; don't create them as just another dumping ground + +### 2) Discover authoritative context (minimal, targeted reading) + +Prioritize: + +* README / docs that describe purpose and setup +* build/test/lint/typecheck configs and scripts: + + * package manifests (package.json, pyproject.toml, Cargo.toml, go.mod, pom.xml, build.gradle, etc.) + * Makefile/Taskfile/justfile + * CI workflows (.github/workflows/* or equivalents) +* repository layout (top-level directories, key packages/apps) + +Be systematic: + +* Use Glob/Grep to locate: + + * "how to run", "development", "testing", "lint", "format", "typecheck", "build", "ci" + * references to dev servers, environment setup, local DBs, etc. + +### 3) Derive a minimal set of "common commands" + +Goal: include only commands that are broadly useful and stable. + +Typical buckets (only include what exists): + +* install / bootstrap +* dev server / local run +* test +* lint / format +* typecheck +* build +* (optional) e2e / integration tests +* (optional) "single test" patterns / targeting + +Validation requirements: + +* For each command you plan to list: + + * confirm it exists (e.g., package.json scripts, Makefile target, CI step) + * if feasible, run a **non-destructive validation** (`--help`, list scripts/targets, or dry-run) + * record status as one of: + + * βœ… executed successfully + * ⚠️ executed but failed (include reason + fix suggestion) + * ⏸️ not executed (include why and how to run) + +**Additional rule for TEST commands (required when feasible):** + +* If you list a test command, you must attempt a **minimal, safe, fast test execution** to prove the harness works. +* Constraints (hard): + + * Avoid anything likely to be **long-running**, **networked**, **E2E**, **integration**, **UI**, or **data-mutating**. + * Do NOT run targets/scripts containing (case-insensitive) keywords like: + + * `e2e`, `integration`, `playwright`, `cypress`, `selenium`, `puppeteer`, `browser` + * `load`, `stress`, `perf`, `benchmark` + * `docker`, `compose`, `k8s`, `helm`, `terraform` + * `migrate`, `seed`, `reset`, `drop`, `provision` +* Selection strategy (in order): + + 1. Prefer an explicitly "fast/unit/smoke/short" target if it exists (e.g., `test:unit`, `test:smoke`, `make test-unit`). + 2. If the test runner supports "list tests / collect only / dry-run", prefer that (it validates wiring without executing). + 3. Otherwise run **one** small unit test file or **one** small test case using runner-specific filtering (choose the smallest/fastest-looking candidate). +* Runtime budget: + + * Keep the minimal test run under ~30s when possible. + * If you can't confidently keep it fast/offline, do **not** run it; mark ⏸️ and explain what you would run locally. +* Reporting: + + * In the validation log, explicitly label test validation as **"minimal test run"** and note how E2E/network/data mutation was avoided. + +### 4) Write or update the onboarding files + +Default: treat `AGENTS.md` as the canonical source. + +**Default file strategy (unless user requested only one target):** + +* Write/update `AGENTS.md` as the source of truth. +* Create/update `CLAUDE.md` as a **symlink to `AGENTS.md`** when feasible. + + * If symlinks are not supported in this environment/repo policy (or would be problematic cross-platform), fall back to keeping the files in sync by copying identical content. + * If you fall back to copying, include a brief note in the files (or in the report) explaining why a symlink was not used. + +Keep the files consistent with each other in either case. + +**CLAUDE.md consolidation workflow:** + +When CLAUDE.md exists as a standalone file (not already a symlink to AGENTS.md): + +1. **Detection**: Check if CLAUDE.md is a symlink (`ls -la CLAUDE.md` or equivalent). If it already points to AGENTS.md, no consolidation needed. + +2. **Interactive modes (quick-start, update, evaluate)**: Ask the user: + > "CLAUDE.md exists as a standalone file. Would you like to consolidate its content into AGENTS.md and replace CLAUDE.md with a symlink? This creates a single source of truth for agent documentation." + + Provide options: Yes (consolidate) / No (keep separate) / Show diff (preview what would be merged) + +3. **Auto mode**: Perform consolidation automatically without asking: + * Read both CLAUDE.md and AGENTS.md (if it exists) + * Merge content intelligently: deduplicate, prefer more accurate/recent content, reconcile contradictions + * Write the consolidated content to AGENTS.md + * Remove the standalone CLAUDE.md + * Create symlink: `ln -s AGENTS.md CLAUDE.md` + * Report exactly what content was merged/changed + +4. **Consolidation merge strategy**: + * If only CLAUDE.md exists: rename to AGENTS.md, create symlink + * If both exist: merge sections by category (WHY/WHAT/HOW), deduplicate commands, keep the most accurate/complete version of each section + * Preserve any CLAUDE.md-specific content that doesn't exist in AGENTS.md + * Note in the report which content came from which source + +If both exist but differ: + +* In update/evaluate/auto, reconcile into one canonical version (prefer the one that is more accurate). +* Remove contradictions; keep the final content consistent. + +Required structure (adapt headings as needed, but keep the intent): + +1. **Project overview (WHY)** + + * 1–3 paragraphs: what it is, who uses it, what "done" means. +2. **Repo map (WHAT)** + + * Bullet list of the few directories/packages that matter most. + * For monorepos: identify apps/services and shared packages. +3. **How to work here (HOW)** + + * Short workflow guidance (explore β†’ plan β†’ implement β†’ validate). + * Explicit validation expectations (tests/typecheck/lint) with validated commands. +4. **Common commands (validated)** + + * Compact list with brief notes (where to run, prerequisites). +5. **Progressive disclosure pointers** + + * Links to authoritative docs or config files (paths). + * Reference any `ai_agent_docs/` files with brief descriptions so Claude can decide which to load. + * Proactively create `ai_agent_docs/` files for cross-cutting topics (architecture, conventions, testing philosophy, etc.) rather than bloating the root AGENTS.md. +6. **Gotchas / invariants** + + * Only high-impact, stable surprises that are not obvious from the code. + +Absolutely avoid: + +* long style guides (prefer "run formatter/linter X") +* huge command lists +* copy-pasted code that will go stale + * Prefer pointing to authoritative files (optionally with path:line references) over copying snippets that may go stale + +#### Progressive disclosure: Nested AGENTS.md vs ai_agent_docs/ + +Two complementary approaches exist for progressive disclosure. Use both as appropriate. + +**Nested AGENTS.md (module-specific docs)**: +* **Preferred for**: Module/service-specific documentation with distinct workflows +* **Auto-discovery**: Claude Code automatically discovers nested CLAUDE.md files +* **Location**: Colocated with the module (e.g., `/config/nvim/AGENTS.md`, `/services/api/AGENTS.md`) +* **Symlink requirement**: Always create `CLAUDE.md` as a symlink to `AGENTS.md` in the same directory (Claude Code loads CLAUDE.md, so the symlink ensures it picks up your AGENTS.md content) +* **Content**: Module-specific commands, workflows, gotchas +* **Structure**: Same WHY/WHAT/HOW format, but scoped to that module + +**ai_agent_docs/ (cross-cutting concerns)**: +* **Preferred for**: Topics that span multiple modules or are architectural in nature +* **Location**: Can exist at any scope level (root or nested within a module) +* **Example topics**: Architecture, conventions, testing philosophy, deployment patterns +* **Reference**: Include in the nearest AGENTS.md's progressive disclosure section + +**Example hierarchy**: +``` +AGENTS.md # Root overview (source of truth) +CLAUDE.md -> AGENTS.md # Symlink for Claude Code discovery +ai_agent_docs/ # Repo-wide cross-cutting +β”œβ”€β”€ architecture.md +└── conventions.md +config/ +β”œβ”€β”€ nvim/ +β”‚ β”œβ”€β”€ AGENTS.md # nvim-specific (source of truth) +β”‚ β”œβ”€β”€ CLAUDE.md -> AGENTS.md # Symlink for Claude Code +β”‚ └── ai_agent_docs/ # nvim cross-cutting (if needed) +β”‚ └── plugin-patterns.md +└── zsh/ + β”œβ”€β”€ AGENTS.md # zsh-specific (source of truth) + └── CLAUDE.md -> AGENTS.md # Symlink for Claude Code +``` + +**Decision guidance**: +* Module has distinct workflows/commands β†’ create nested `AGENTS.md` +* Topic spans modules or is architectural β†’ use `ai_agent_docs/` +* Keep both concise; prefer colocated AGENTS.md when in doubt + +**When to create**: +* **Auto mode**: Proactively create nested AGENTS.md when processing multiple scopes; create ai_agent_docs/ for cross-cutting concerns. Always create CLAUDE.md symlinks alongside each AGENTS.md. +* **Interactive modes**: Suggest based on complexity; ask user which approach fits their mental model + +**Content guidelines** (applies to both): +* Keep each file focused and concise (target <100 lines for ai_agent_docs/, <150 for nested AGENTS.md) +* Use `file:line` references instead of copying code +* Each file should be self-contained for its topic +* Include the same validation markers (βœ…/⚠️/⏸️) for any commands + +**Validation**: Same rules as main docsβ€”validate commands, confirm paths exist, mark execution status. + +**Referencing in AGENTS.md**: Include a "Task-Specific Docs" section: +```markdown +## Task-Specific Docs + +Nested module docs (auto-discovered by Claude Code): +- `config/nvim/AGENTS.md` - Neovim configuration +- `config/zsh/AGENTS.md` - Zsh shell setup + +Cross-cutting docs in `ai_agent_docs/`: +- `ai_agent_docs/architecture.md` - System design and key abstractions +- `ai_agent_docs/conventions.md` - Code style and patterns +``` + +This allows Claude to selectively load only the context needed for the current task. + +### 5) Evaluate (evaluate / auto) + +Provide a short rubric report (in chat), covering: + +* Conciseness and universality (what was removed or moved out) +* Correctness (what was validated, what was stale) +* Progressive disclosure quality +* Monorepo clarity +* AGENTS vs CLAUDE consistency + +In auto mode: apply improvements directly. + +### 6) Report back (all modes) + +In chat, output: + +* Files created/updated (paths) +* Key changes (bullets) +* Validation log (commands + βœ… / ⚠️ / ⏸️) +* **Symlink status**: For each AGENTS.md created/updated, report whether CLAUDE.md symlink exists: + * βœ… `path/CLAUDE.md -> AGENTS.md` (symlink exists) + * ⚠️ `path/CLAUDE.md` missing symlink (created it / needs manual creation) + * Remind user: "AGENTS.md is the source of truth. Claude Code discovers CLAUDE.md files, so the symlink ensures your docs are loaded." +* Remaining unknowns or follow-ups (if any) + +Do **NOT** paste full file contents unless the user asks. Summarize and point to the files. + +--- + +## Now execute + +Proceed in the parsed mode, using the scope rules above. + +This command is designed to keep AGENTS.md and CLAUDE.md short, accurate, and universally useful, while enforcing **"validate what you write"** with concrete, safe, and minimal command execution. From 84551197e7805b04f560591257914750d5cb683a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 20:40:42 +0000 Subject: [PATCH 4/4] Refactor to skills-based architecture with single generic workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces per-agent reusable workflows and flat prompts/ files with a single generic skill runner and structured SKILL.md files following the Claude Code skill convention. Skills (replaces prompts/): - skills/codex-code-review/SKILL.md β€” frontmatter + verbatim Codex body - skills/context-files/SKILL.md β€” frontmatter + full context files prompt Generic workflow (replaces claude_pr_review.yml + context_files_agent.yml): - .github/workflows/claude_pr_agent.yml β€” accepts skill: (name or path), args: ($ARGUMENTS substitution), prompt: (raw bypass); resolves bare names from central repo skills/ and paths from target repo checkout; strips YAML frontmatter via awk before sending body to Claude Skill resolution: "codex-code-review" β†’ skills/codex-code-review/SKILL.md (central) ".claude/skills/my-skill" β†’ .claude/skills/my-skill/SKILL.md (target repo) "./skills/my-skill" β†’ skills/my-skill/SKILL.md (target repo) "./skills/my-skill.md" β†’ flat file, no directory (target repo) Sequencing via needs:: caller workflows chain skills as separate jobs; each job calls claude_pr_agent.yml with a single skill; needs: enforces ordering without requiring dynamic steps. Updated callers: - ai_pr_review.yml β†’ skill: "codex-code-review" - context_files_pr.yml β†’ skill: "context-files", args: "auto ." Updated docs: - docs/agents/index.md β€” skills pattern and shared workflow explanation - docs/agents/pr-review.md β€” skill frontmatter, local skill path override, sequencing example with needs: - docs/agents/context-files.md β€” args: input, local skill path override - docs/contributing.md β€” two-file pattern (SKILL.md + doc page, no new workflow) - docs/using.md β€” skill: input, local skill paths, mixed central+local example https://claude.ai/code/session_018moumKtzZQiSiyGxVUFGdS --- .github/workflows/ai_pr_review.yml | 15 +- .github/workflows/claude_pr_agent.yml | 112 ++++++++++++ .github/workflows/claude_pr_review.yml | 55 ------ .github/workflows/context_files_agent.yml | 64 ------- .github/workflows/context_files_pr.yml | 15 +- AGENTS.md | 47 ++--- docs/agents/context-files.md | 65 ++++--- docs/agents/index.md | 42 +++-- docs/agents/pr-review.md | 81 +++++++-- docs/contributing.md | 171 ++++++++---------- docs/using.md | 95 ++++++---- .../codex-code-review/SKILL.md | 7 + .../context-files/SKILL.md | 6 +- 13 files changed, 437 insertions(+), 338 deletions(-) create mode 100644 .github/workflows/claude_pr_agent.yml delete mode 100644 .github/workflows/claude_pr_review.yml delete mode 100644 .github/workflows/context_files_agent.yml rename prompts/codex_code_review_prompt.md => skills/codex-code-review/SKILL.md (75%) rename prompts/context_files_prompt.md => skills/context-files/SKILL.md (98%) diff --git a/.github/workflows/ai_pr_review.yml b/.github/workflows/ai_pr_review.yml index b90cff4..3c6b639 100644 --- a/.github/workflows/ai_pr_review.yml +++ b/.github/workflows/ai_pr_review.yml @@ -9,12 +9,17 @@ concurrency: cancel-in-progress: true jobs: - scan: + review: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "codex-code-review" secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - # Optional: override prompt per repo: + # Optional: run a local skill from the target repo instead: # with: - # prompt_override: | - # + # skill: ".claude/skills/my-custom-review" + # Optional: use a raw prompt instead of a skill file: + # with: + # prompt: | + # Review this diff for security issues only. diff --git a/.github/workflows/claude_pr_agent.yml b/.github/workflows/claude_pr_agent.yml new file mode 100644 index 0000000..52069c3 --- /dev/null +++ b/.github/workflows/claude_pr_agent.yml @@ -0,0 +1,112 @@ +name: Claude PR agent (skill runner) + +on: + workflow_call: + inputs: + skill: + description: | + Skill to run. Either: + - A bare name resolved from this repo's skills/ directory: + "codex-code-review" β†’ skills/codex-code-review/SKILL.md + "context-files" β†’ skills/context-files/SKILL.md + - A target-repo-relative path to a skill directory or file: + ".claude/skills/my-skill" β†’ .claude/skills/my-skill/SKILL.md + "./skills/my-skill" β†’ skills/my-skill/SKILL.md + "./skills/my-skill.md" β†’ skills/my-skill.md (flat prompt) + Rule: values containing "/" are resolved from the target repo checkout; + bare names are resolved from this central workflow repo. + type: string + required: false + default: "" + args: + description: | + Substituted for $ARGUMENTS in the skill body. + Example: "auto ." for the context-files skill. + type: string + required: false + default: "" + prompt: + description: | + Raw prompt text used as-is. Alternative to `skill` for quick + one-off instructions without a skill file. + type: string + required: false + default: "" + secrets: + ANTHROPIC_API_KEY: + required: true + +jobs: + run-skill: + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + + steps: + # Checkout the *target repository* so Claude can read the codebase. + - name: Checkout target repo + uses: actions/checkout@v4 + + # Checkout this workflow repo to access the skills/ directory. + - name: Checkout workflow repo (for skills) + uses: actions/checkout@v4 + with: + repository: safurrier/python-collab-template + path: _ai_workflows + + - name: Load skill + id: skill + shell: bash + run: | + SKILL="${{ inputs.skill }}" + ARGS="${{ inputs.args }}" + PROMPT="${{ inputs.prompt }}" + + write_output() { + echo "text<> "$GITHUB_OUTPUT" + printf "%s\n" "$1" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + } + + # Raw prompt mode β€” use as-is + if [ -z "$SKILL" ] && [ -n "$PROMPT" ]; then + write_output "$PROMPT" + exit 0 + fi + + if [ -z "$SKILL" ]; then + echo "Error: either 'skill' or 'prompt' input must be provided." >&2 + exit 1 + fi + + # Resolve skill file path + if [[ "$SKILL" == */* ]]; then + # Contains a slash β€” path in the target repo + if [ -d "$SKILL" ]; then + skill_file="${SKILL}/SKILL.md" # directory-style skill + else + skill_file="$SKILL" # direct .md file + fi + else + # Bare name β€” central repo skill + skill_file="_ai_workflows/skills/${SKILL}/SKILL.md" + fi + + if [ ! -f "$skill_file" ]; then + echo "Error: skill file not found: $skill_file" >&2 + exit 1 + fi + + # Yank (strip) YAML frontmatter, then substitute $ARGUMENTS + body=$(awk 'BEGIN{fm=0} /^---$/{fm++; next} fm<2{next} {print}' "$skill_file") + body="${body//\$ARGUMENTS/$ARGS}" + + write_output "$body" + + - name: Run Claude + uses: anthropics/claude-code-action@v1 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + prompt: ${{ steps.skill.outputs.text }} diff --git a/.github/workflows/claude_pr_review.yml b/.github/workflows/claude_pr_review.yml deleted file mode 100644 index e6992e9..0000000 --- a/.github/workflows/claude_pr_review.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Claude PR review (Codex prompt) - -on: - workflow_call: - inputs: - prompt_override: - type: string - required: false - default: "" - secrets: - ANTHROPIC_API_KEY: - required: true - -jobs: - review: - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - issues: write - - steps: - # Checkout the *target repository* (the caller repo) so Claude can see the code/diff context. - - name: Checkout target repo - uses: actions/checkout@v4 - - # Checkout this workflow repo to read the prompt file. - - name: Checkout workflow repo (for prompt) - uses: actions/checkout@v4 - with: - repository: safurrier/python-collab-template - path: _ai_workflows - - - name: Load prompt - id: prompt - shell: bash - run: | - if [ -n "${{ inputs.prompt_override }}" ]; then - echo "text<> "$GITHUB_OUTPUT" - printf "%s\n" "${{ inputs.prompt_override }}" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - else - echo "text<> "$GITHUB_OUTPUT" - cat _ai_workflows/prompts/codex_code_review_prompt.md >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - fi - - - name: Run Claude review - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: ${{ steps.prompt.outputs.text }} - # Optional knobs (uncomment if desired): - # use_sticky_comment: "true" - # claude_args: "--max-turns 5" diff --git a/.github/workflows/context_files_agent.yml b/.github/workflows/context_files_agent.yml deleted file mode 100644 index 9d70030..0000000 --- a/.github/workflows/context_files_agent.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Context files agent (AGENTS.md / CLAUDE.md) - -on: - workflow_call: - inputs: - agent_args: - description: > - Arguments passed to the context files prompt (mode + scope). - Defaults to "auto ." which runs in auto mode on the repo root. - type: string - required: false - default: "auto ." - prompt_override: - description: > - Full prompt text to use instead of the default context files prompt. - Overrides agent_args when set. - type: string - required: false - default: "" - secrets: - ANTHROPIC_API_KEY: - required: true - -jobs: - context-files: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - issues: write - - steps: - # Checkout the *target repository* so Claude can read the codebase. - - name: Checkout target repo - uses: actions/checkout@v4 - - # Checkout this workflow repo to read the prompt template. - - name: Checkout workflow repo (for prompt) - uses: actions/checkout@v4 - with: - repository: safurrier/python-collab-template - path: _ai_workflows - - - name: Load prompt - id: prompt - shell: bash - run: | - if [ -n "${{ inputs.prompt_override }}" ]; then - echo "text<> "$GITHUB_OUTPUT" - printf "%s\n" "${{ inputs.prompt_override }}" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - else - ARGS="${{ inputs.agent_args }}" - echo "text<> "$GITHUB_OUTPUT" - sed "s|\$ARGUMENTS|${ARGS}|g" \ - _ai_workflows/prompts/context_files_prompt.md >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - fi - - - name: Run context files agent - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: ${{ steps.prompt.outputs.text }} diff --git a/.github/workflows/context_files_pr.yml b/.github/workflows/context_files_pr.yml index 65298f8..27195b2 100644 --- a/.github/workflows/context_files_pr.yml +++ b/.github/workflows/context_files_pr.yml @@ -11,11 +11,16 @@ concurrency: jobs: context-files: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "context-files" + args: "auto ." secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - # Optional: pass explicit mode/scope or a full prompt override: + # Optional: scope to a subdirectory: # with: - # agent_args: "auto src/" - # prompt_override: | - # + # skill: "context-files" + # args: "auto src/" + # Optional: use a local skill from the target repo: + # with: + # skill: ".claude/skills/context-files" diff --git a/AGENTS.md b/AGENTS.md index 5a0383a..d2df28c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,15 +12,16 @@ The repo is intentionally minimal: workflow YAML, prompt files, and documentatio ``` .github/workflows/ - claude_pr_review.yml # Reusable: code review agent (workflow_call) - context_files_agent.yml # Reusable: AGENTS.md / CLAUDE.md generator (workflow_call) + claude_pr_agent.yml # Reusable: generic skill runner (workflow_call) ai_pr_review.yml # Caller example: code review for this repo context_files_pr.yml # Caller example: context files agent for this repo docs.yml # MkDocs build + GitHub Pages deploy -prompts/ - codex_code_review_prompt.md # Verbatim Codex Code Review prompt (OpenAI cookbook) - context_files_prompt.md # Context files generator/updater prompt +skills/ + codex-code-review/ + SKILL.md # Codex Code Review prompt (verbatim body + frontmatter) + context-files/ + SKILL.md # Context files generator/updater prompt (with $ARGUMENTS) docs/ # MkDocs documentation site source index.md # Overview and architecture @@ -29,8 +30,8 @@ docs/ # MkDocs documentation site source contributing.md # How to add a new agent agents/ index.md # Agent catalog - pr-review.md # Code review agent docs - context-files.md # Context files agent docs + pr-review.md # Code review skill docs + context-files.md # Context files skill docs ``` --- @@ -39,17 +40,18 @@ docs/ # MkDocs documentation site source ### Adding a new agent -1. Create `prompts/.md` β€” the Claude instruction text -2. Create `.github/workflows/.yml` β€” copy an existing reusable workflow, swap the prompt filename, adjust permissions if the agent writes files -3. Create `docs/agents/.md` β€” trigger table, caller snippet, customization options -4. Add entry to `docs/agents/index.md` and `mkdocs.yml` nav -5. Tag a new release: `git tag vX.Y && git push origin vX.Y` +1. Create `skills//SKILL.md` β€” YAML frontmatter + prompt body (frontmatter is stripped before Claude sees it) +2. Create `docs/agents/.md` β€” trigger table, caller snippet, customization options +3. Add entry to `docs/agents/index.md` and `mkdocs.yml` nav +4. Tag a new release: `git tag vX.Y && git push origin vX.Y` + +No new workflow file needed β€” `claude_pr_agent.yml` handles all skills generically. See `docs/contributing.md` for the full checklist. -### Updating a prompt +### Updating a skill -Edit the file in `prompts/`. Bump the version tag so target repos can opt in to the updated prompt on their own schedule. +Edit `skills//SKILL.md`. Bump the version tag so target repos can opt in to the updated skill on their own schedule. ### Working on docs @@ -90,18 +92,19 @@ No test suite, linter, or build step for the workflows themselves β€” validation ## Progressive disclosure - `docs/using.md` β€” full setup walkthrough for target repos -- `docs/contributing.md` β€” step-by-step agent contribution guide -- `docs/agents/pr-review.md` β€” code review agent: triggers, prompt override, permissions -- `docs/agents/context-files.md` β€” context files agent: modes, `agent_args`, permissions -- `.github/workflows/claude_pr_review.yml` β€” canonical source for reusable workflow structure -- `prompts/context_files_prompt.md` β€” canonical source for context files prompt spec +- `docs/contributing.md` β€” step-by-step skill contribution guide +- `docs/agents/pr-review.md` β€” review skill: triggers, local skill override, sequencing +- `docs/agents/context-files.md` β€” context files skill: modes, `args`, local override +- `.github/workflows/claude_pr_agent.yml` β€” canonical source for the generic skill runner +- `skills/context-files/SKILL.md` β€” canonical source for context files skill spec --- ## Gotchas -- **Repo rename breaks callers**: the reusable workflows hard-code `repository: safurrier/python-collab-template` to check out the prompt files. If this repo is renamed, update that field in every reusable workflow. +- **Repo rename breaks callers**: `claude_pr_agent.yml` hard-codes `repository: safurrier/python-collab-template` to check out the skills. If this repo is renamed, update that field. - **Private repo access**: if this repo is private, target repos must be granted access via Settings β†’ Actions β†’ Access β†’ "Accessible from repositories in your account". - **Tag before using**: target repos reference `@v1` (or another tag). Push a tag before pointing any repo at this one. -- **`contents: write` scope**: the context files agent needs `contents: write` to commit `AGENTS.md` / `CLAUDE.md`. The review agent only needs `read`. -- **`$ARGUMENTS` substitution**: the context files prompt uses a `$ARGUMENTS` placeholder that the workflow substitutes via `sed` at runtime. Do not treat it as a shell variable in the prompt file itself. +- **`contents: write` always on**: the generic workflow always requests `contents: write` so any skill can commit files. This is intentional. +- **`$ARGUMENTS` substitution**: skills use a `$ARGUMENTS` placeholder that the workflow substitutes via `sed` at runtime using the `args:` input. It is not a shell variable β€” do not use `${ARGUMENTS}` or `$ARGS`. +- **Frontmatter stripping**: the `awk` command in the workflow strips everything between the first and second `---` blocks. The skill body starts on the line after the closing `---`. diff --git a/docs/agents/context-files.md b/docs/agents/context-files.md index 02cbb35..4e45476 100644 --- a/docs/agents/context-files.md +++ b/docs/agents/context-files.md @@ -6,8 +6,21 @@ Keeps `AGENTS.md` and `CLAUDE.md` up to date in a repository. When these files a | File | Path | |---|---| -| Reusable workflow | `.github/workflows/context_files_agent.yml` | -| Prompt | `prompts/context_files_prompt.md` | +| Skill | `skills/context-files/SKILL.md` | +| Workflow (shared) | `.github/workflows/claude_pr_agent.yml` | + +## Skill frontmatter + +```yaml +--- +name: context-files +description: Create, update, or evaluate AGENTS.md and CLAUDE.md files for a repository. Follows WHY/WHAT/HOW structure with validated commands and progressive disclosure. +argument-hint: "[mode] [scope]" +allowed-tools: Read, Write, Edit, Bash, Grep, Glob +--- +``` + +The `argument-hint` documents what the `args:` workflow input expects. The frontmatter is stripped at load time β€” Claude only sees the body. ## What it does @@ -17,16 +30,16 @@ Claude reads the repo structure, config files, CI workflows, and any existing co 2. **If files already exist** (`auto` β†’ `update + evaluate` mode): validates commands, removes stale content, improves conciseness, and creates nested `AGENTS.md` files for subdirectories with distinct workflows 3. **Commits the result** directly to the PR branch so the updated context files are part of the PR -The prompt follows a strict principle: **validate what you write**. Every command in the generated docs is confirmed to exist in Makefiles, CI configs, or package manifests before being included. +Every command in the generated docs is validated against Makefiles, CI configs, or package manifests before being included. ## Output files | File | Role | |---|---| -| `AGENTS.md` | Source of truth for agent onboarding β€” WHY/WHAT/HOW, validated commands, progressive disclosure pointers | -| `CLAUDE.md` | Symlink to `AGENTS.md` (Claude Code auto-discovers `CLAUDE.md` files when navigating a repo) | -| `ai_agent_docs/*.md` | Cross-cutting topic docs (architecture, conventions, etc.) created when content warrants it | -| `/AGENTS.md` | Nested docs for subdirectories with their own distinct tooling | +| `AGENTS.md` | Source of truth β€” WHY/WHAT/HOW, validated commands, progressive disclosure pointers | +| `CLAUDE.md` | Symlink to `AGENTS.md` (Claude Code auto-discovers `CLAUDE.md` files) | +| `ai_agent_docs/*.md` | Cross-cutting topic docs created when content warrants it | +| `/AGENTS.md` | Nested docs for subdirectories with distinct tooling | ## Triggers @@ -34,7 +47,7 @@ The prompt follows a strict principle: **validate what you write**. Every comman |---|---| | PR first opened (non-draft) | Yes | | PR marked Ready for review | Yes | -| New commits pushed to open PR | No (use review agent for that) | +| New commits pushed to open PR | No | | Draft PR | No | ## Minimal caller workflow @@ -53,20 +66,24 @@ concurrency: jobs: context-files: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "context-files" + args: "auto ." secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` ## Customizing scope or mode -Use `agent_args` to change which directory Claude targets or which mode it runs in: +Pass mode and scope via the `args:` input (substituted for `$ARGUMENTS` in the skill): ```yaml with: - agent_args: "auto src/" # scope to src/ only - # agent_args: "quick-start ." # always generate fresh, never update - # agent_args: "evaluate ." # audit and propose improvements, report in chat + skill: "context-files" + args: "auto src/" # scope to src/ only + # args: "quick-start ." # always generate fresh, never update + # args: "evaluate ." # audit and report in chat without writing ``` Modes: @@ -74,17 +91,25 @@ Modes: | Mode | Behavior | |---|---| | `auto` (default) | Generate if missing; update + evaluate if present. No confirmation prompts. | -| `quick-start` | Always generate from scratch. Asks before overwriting. | +| `quick-start` | Generate from scratch. Asks before overwriting existing files. | | `update` | Update existing files only. | -| `evaluate` | Audit files against quality principles and propose improvements. | +| `evaluate` | Audit and propose improvements without modifying files. | -## Full prompt override +## Use a local skill from the target repo -To replace the entire prompt (e.g., for a simpler or domain-specific context file format): +Target repos can override the central skill entirely with their own `SKILL.md`: ```yaml with: - prompt_override: | + skill: ".claude/skills/context-files" # loads from target repo + args: "auto ." +``` + +Or use a raw prompt for a simpler custom format: + +```yaml +with: + prompt: | Look at this repo and create AGENTS.md with: 1. A one-sentence description of what the repo does 2. How to run tests @@ -96,9 +121,7 @@ with: ```yaml permissions: - contents: write # needed to commit AGENTS.md / CLAUDE.md to the branch + contents: write # required: commits AGENTS.md / CLAUDE.md to the branch pull-requests: write issues: write ``` - -Unlike the review agent, this one needs `contents: write` because it commits files. diff --git a/docs/agents/index.md b/docs/agents/index.md index 7fa4a26..8295d5d 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -1,32 +1,42 @@ # Agents -Each agent is a self-contained pair of files: +Each agent is a **skill** β€” a markdown file with YAML frontmatter β€” run by one generic reusable workflow. -| File | Purpose | -|---|---| -| `prompts/.md` | The instruction text sent to Claude | -| `.github/workflows/.yml` | The reusable `workflow_call` that loads the prompt and runs the action | +``` +skills/ + / + SKILL.md ← frontmatter (metadata) + body (prompt sent to Claude) -Target repos reference the reusable workflow with a `uses:` line and supply `ANTHROPIC_API_KEY`. The prompt loading, checkout, and Claude invocation all happen inside the reusable workflow here. +.github/workflows/ + claude_pr_agent.yml ← single generic workflow_call for all skills +``` + +Target repos reference `claude_pr_agent.yml` and pass a `skill:` name. The workflow checks out this central repo, strips the frontmatter from the skill file, substitutes any `$ARGUMENTS`, and runs `anthropics/claude-code-action`. --- -## Available agents +## Available skills -| Agent | Reusable workflow | Prompt file | +| Skill | Directory | What it does | |---|---|---| -| [PR Code Review](pr-review.md) | `claude_pr_review.yml` | `codex_code_review_prompt.md` | -| [Context Files](context-files.md) | `context_files_agent.yml` | `context_files_prompt.md` | +| [PR Code Review](pr-review.md) | `skills/codex-code-review/` | Reviews diff; posts findings and correctness verdict | +| [Context Files](context-files.md) | `skills/context-files/` | Creates or updates `AGENTS.md` / `CLAUDE.md` | --- ## Shared design principles -**Every agent:** +**Every skill:** + +- Has YAML frontmatter (stripped before Claude sees it) with `name`, `description`, `argument-hint`, `allowed-tools` +- Uses `$ARGUMENTS` for runtime substitution when parameterizable +- Is independent β€” skills don't depend on each other + +**The generic workflow (`claude_pr_agent.yml`):** -- Accepts a `prompt_override` input so any repo can swap in custom instructions without forking -- Uses the same checkout pattern: target repo first, then this workflow repo for the prompt -- Runs on `ubuntu-latest` with least-privilege permissions (only escalating `contents` to `write` when the agent needs to commit files) -- Can be called independently β€” agents don't depend on each other +- Accepts a `skill:` name (central repo) or a target-repo-relative path (local skill) +- Accepts `args:` for `$ARGUMENTS` substitution +- Accepts `prompt:` as a raw prompt bypass (no skill file needed) +- Always has `contents: write` so any skill can commit files if needed -**Prompt files** live in `prompts/` and are loaded at runtime, not embedded in the workflow YAML. This means you can update a prompt and tag a new release without touching any workflow logic. +**Adding a new agent** = adding a `skills//SKILL.md` file + a doc page. No new workflow needed. See [Contributing](../contributing.md). diff --git a/docs/agents/pr-review.md b/docs/agents/pr-review.md index e63b5de..b674bfe 100644 --- a/docs/agents/pr-review.md +++ b/docs/agents/pr-review.md @@ -6,8 +6,21 @@ Reviews PR diffs using the official [Codex Code Review prompt](https://developer | File | Path | |---|---| -| Reusable workflow | `.github/workflows/claude_pr_review.yml` | -| Prompt | `prompts/codex_code_review_prompt.md` | +| Skill | `skills/codex-code-review/SKILL.md` | +| Workflow (shared) | `.github/workflows/claude_pr_agent.yml` | + +## Skill frontmatter + +```yaml +--- +name: codex-code-review +description: Reviews PR diffs using the official Codex Code Review prompt. Posts actionable findings by category and a correctness verdict with confidence score. +argument-hint: "" +allowed-tools: Read, Grep, Glob, Bash +--- +``` + +The body after the frontmatter is the verbatim Codex prompt text. The frontmatter is stripped at load time β€” Claude only sees the body. ## What it does @@ -19,7 +32,7 @@ Claude reads the PR diff and flags issues in these categories: - **Maintainability** β€” API misuse, dead code, structural problems - **Developer experience** β€” confusing interfaces, missing context -It then produces an overall verdict β€” `patch is correct` or `patch is incorrect` β€” with a confidence score between 0 and 1. +It produces an overall verdict β€” `patch is correct` or `patch is incorrect` β€” with a confidence score between 0 and 1. ## Triggers @@ -46,46 +59,74 @@ concurrency: jobs: review: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "codex-code-review" secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` -## Customizing the prompt +## Customization options + +### Use a local skill from the target repo + +Target repos can ship their own `SKILL.md` and pass its path instead of a central skill name: + +```yaml +with: + skill: ".claude/skills/security-review" # loads .claude/skills/security-review/SKILL.md +``` + +This follows the standard Claude Code skill directory convention β€” the `SKILL.md` file is in a named subdirectory. + +### Use a raw prompt (no skill file) -The `prompt_override` input replaces the entire default prompt. This makes the workflow a generic "run Claude on a PR with any prompt" mechanism β€” the code review prompt just happens to be the default. +For a one-off instruction without creating a skill file: + +```yaml +with: + prompt: | + Review this diff for REST API convention violations: + - snake_case paths only + - responses must have a top-level "data" key + Flag each violation with file and line. Give a pass/fail verdict. +``` + +### Run multiple skills sequentially + +Use `needs:` to chain skills across jobs. Each job calls the same generic workflow with a different skill: ```yaml jobs: review: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "codex-code-review" secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + + conventions: + needs: review + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 with: - prompt_override: | - Review this diff for any violation of our REST API conventions: - - All endpoints must use snake_case paths - - Response envelopes must include a top-level "data" key - - Error responses must include "code" and "message" fields - Flag each violation with file and line number. Give a pass/fail verdict. + skill: ".claude/skills/team-conventions" + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` -Because the workflow is generic, you can use `prompt_override` to turn this into any kind of PR agent β€” a changelog enforcer, a migration validator, a documentation checker β€” without adding a new workflow. - ## Permissions ```yaml permissions: - contents: read + contents: write pull-requests: write issues: write ``` -`pull-requests: write` and `issues: write` are needed for Claude to post comments to the PR. - -## Default prompt +`contents: write` is present on the shared workflow for compatibility with write-enabled skills. The review skill itself doesn't write files. -The prompt is the verbatim text from the OpenAI Codex cookbook. Source: [prompts/codex_code_review_prompt.md](https://github.com/safurrier/python-collab-template/blob/main/prompts/codex_code_review_prompt.md) +## Skill body (verbatim) > You are acting as a reviewer for a proposed code change made by another engineer. Focus on issues that impact correctness, performance, security, maintainability, or developer experience. Flag only actionable issues introduced by the pull request. When you flag an issue, provide a short, direct explanation and cite the affected file and line range. Prioritize severe issues and avoid nit-level comments unless they block understanding of the diff. After listing findings, produce an overall correctness verdict ("patch is correct" or "patch is incorrect") with a concise justification and a confidence score between 0 and 1. Ensure that file citations and line numbers are exactly correct using the tools available; if they are incorrect your comments will be rejected. diff --git a/docs/contributing.md b/docs/contributing.md index f34bacf..73ec268 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -1,110 +1,72 @@ # Contributing a New Agent -Adding an agent takes three files and a tag bump. No framework, no registration step. +Adding an agent takes **two files and a tag bump** β€” a skill file and a doc page. No new workflow needed. ## The pattern -Every agent is: +``` +skills/ + / + SKILL.md ← frontmatter + prompt body -1. A **prompt file** in `prompts/` β€” the instruction text Claude receives -2. A **reusable workflow** in `.github/workflows/` β€” the Actions plumbing -3. A **doc page** in `docs/agents/` β€” how to use it and what it does +docs/agents/ + .md ← doc page +``` -That's it. The reusable workflow is almost identical for every agent; you're mostly just changing the prompt file name and adjusting permissions if the agent needs to write files. +The generic `claude_pr_agent.yml` workflow already handles loading, frontmatter stripping, `$ARGUMENTS` substitution, and running Claude. You only need to write the skill content. --- -## Step 1 β€” Write the prompt - -Create `prompts/.md`. +## Step 1 β€” Write the skill -Write the prompt as you'd write a Claude system prompt or slash-command definition. A few guidelines: - -- Be specific about the **output format** (what Claude should post, commit, or return) -- Describe the **scope** (what Claude should read β€” diff only, full repo, specific files) -- If the prompt is parameterizable (like context files' mode/scope), use a `$ARGUMENTS` placeholder and document the substitution in the workflow -- Keep it focused β€” one agent, one job +Create `skills//SKILL.md`: +```markdown +--- +name: your-skill-name +description: One sentence describing what this skill does and when to use it. +argument-hint: "" +allowed-tools: Read, Grep, Glob, Bash --- -## Step 2 β€” Create the reusable workflow - -Create `.github/workflows/.yml`. +Your prompt body here. This is what Claude receives β€” the frontmatter above +is stripped before the body is sent. -Use the existing agents as templates. The structure is always: +Write the prompt as you'd write a Claude system prompt: +- Be specific about the output format (what Claude should post, commit, or return) +- Describe the scope (diff only? full repo? specific files?) +- If parameterizable, use $ARGUMENTS and document it in argument-hint +- Keep it focused β€” one skill, one job +``` -```yaml -name: +**Frontmatter fields:** -on: - workflow_call: - inputs: - prompt_override: - type: string - required: false - default: "" - # Add any agent-specific inputs here (e.g., agent_args) - secrets: - ANTHROPIC_API_KEY: - required: true +| Field | Required | Notes | +|---|---|---| +| `name` | Recommended | Machine name, matches directory name | +| `description` | Recommended | One line; shown in docs and used by Claude Code for auto-invocation | +| `argument-hint` | If using `$ARGUMENTS` | Documents what the `args:` input expects, e.g. `"[mode] [scope]"` | +| `allowed-tools` | Optional | Documents which Claude tools the skill uses | -jobs: - : - runs-on: ubuntu-latest - permissions: - contents: read # bump to write if the agent commits files - pull-requests: write - issues: write - - steps: - - name: Checkout target repo - uses: actions/checkout@v4 - - - name: Checkout workflow repo (for prompt) - uses: actions/checkout@v4 - with: - repository: safurrier/python-collab-template - path: _ai_workflows - - - name: Load prompt - id: prompt - shell: bash - run: | - if [ -n "${{ inputs.prompt_override }}" ]; then - echo "text<> "$GITHUB_OUTPUT" - printf "%s\n" "${{ inputs.prompt_override }}" >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - else - echo "text<> "$GITHUB_OUTPUT" - cat _ai_workflows/prompts/.md >> "$GITHUB_OUTPUT" - echo "EOF" >> "$GITHUB_OUTPUT" - fi - - - name: Run - uses: anthropics/claude-code-action@v1 - with: - anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - prompt: ${{ steps.prompt.outputs.text }} -``` +The frontmatter is **metadata for the framework** β€” it's stripped ("yanked") before Claude sees the content. Only the markdown body goes to Claude. -**Key decisions:** +**If the skill uses `$ARGUMENTS`:** the workflow substitutes the `args:` input at runtime via `sed`. Default `args` is empty string if not provided. -- `contents: read` vs `contents: write` β€” use `write` only if the agent commits files back to the branch -- Add extra inputs (like `agent_args`) if the prompt is parameterizable -- Use `sed "s|\$ARGUMENTS|${ARGS}|g"` in the Load prompt step when substituting arguments into the prompt +**Permissions:** `claude_pr_agent.yml` always requests `contents: write`. No changes needed for skills that only read; skills that commit files just work. --- -## Step 3 β€” Write the doc page +## Step 2 β€” Write the doc page -Create `docs/agents/.md`. Include: +Create `docs/agents/.md`. Include: - What it does (1–2 sentences) -- The files table (reusable workflow + prompt path) -- Trigger event table +- Files table (skill path + shared workflow) +- Skill frontmatter block (for reference) +- Trigger event table (what events make sense for this skill) - Minimal caller workflow (copy-paste ready) -- Customization options (`prompt_override`, any extra inputs) -- Permissions block +- Customization options (`args:`, `skill:` path override, `prompt:` bypass) +- Permissions note Then add it to the nav in `mkdocs.yml`: @@ -114,36 +76,57 @@ nav: - Overview: agents/index.md - PR Code Review: agents/pr-review.md - Context Files: agents/context-files.md - - Your Agent: agents/your-agent-name.md # add this line + - Your Skill: agents/your-skill-name.md # add this ``` And add a row to the table in `docs/agents/index.md`. --- -## Step 4 β€” Tag a new release +## Step 3 β€” Tag a new release ```bash -git add prompts/.md \ - .github/workflows/.yml \ - docs/agents/.md \ - docs/agents/index.md \ - mkdocs.yml -git commit -m "feat: add agent" -git tag v1.1 # or next semver +git add skills// docs/agents/.md docs/agents/index.md mkdocs.yml +git commit -m "feat: add skill" +git tag v1.1 # bump to next semver git push origin main --tags ``` -Target repos that want the new agent update their caller workflow to reference `@v1.1`. +Target repos update their `uses: ...@v1` β†’ `@v1.1` to pick up the new skill. + +--- + +## Minimal caller workflow for a new skill + +```yaml +name: Your skill name + +on: + pull_request: + types: [ready_for_review, synchronize, reopened] + +concurrency: + group: your-skill-${{ github.repository }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + your-skill: + if: ${{ github.event.pull_request.draft == false }} + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "your-skill-name" + # args: "some args" # if skill uses $ARGUMENTS + secrets: + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} +``` --- ## Checklist -- [ ] `prompts/.md` β€” prompt written and reviewed -- [ ] `.github/workflows/.yml` β€” reusable workflow using `workflow_call` -- [ ] `prompt_override` input present with `default: ""` -- [ ] Permissions set to minimum required (`contents: write` only if needed) +- [ ] `skills//SKILL.md` β€” frontmatter + prompt body +- [ ] `name` field matches directory name +- [ ] `argument-hint` set if skill uses `$ARGUMENTS` - [ ] `docs/agents/.md` β€” doc page with trigger table and caller snippet - [ ] Row added to `docs/agents/index.md` - [ ] Entry added to `mkdocs.yml` nav diff --git a/docs/using.md b/docs/using.md index 76dc268..994e31f 100644 --- a/docs/using.md +++ b/docs/using.md @@ -1,6 +1,6 @@ # Using in Your Repo -Each agent is independent. Add only the ones you want. +Each skill is independent β€” add only the ones you want. All skills run through the same generic `claude_pr_agent.yml` workflow. ## Prerequisites @@ -23,11 +23,11 @@ Future releases follow the same pattern: `v1.1`, `v2`, etc. Target repos stay pi --- -## Add an agent to a repo +## Add skills to a repo -### Step 1 β€” Create the caller workflow file +### Step 1 β€” Create the caller workflow -Create `.github/workflows/ai_pr_review.yml` in your target repo with the agents you want. Each agent is a separate job. +Create `.github/workflows/claude_agents.yml` in your target repo. Each skill is a separate job pointing at the same `claude_pr_agent.yml` workflow. **Code review only:** @@ -45,7 +45,9 @@ concurrency: jobs: review: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "codex-code-review" secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` @@ -66,12 +68,15 @@ concurrency: jobs: context-files: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "context-files" + args: "auto ." secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` -**Both agents together** β€” put them in one file as separate jobs: +**Both skills, sequenced** β€” review runs first, context-files waits for it: ```yaml name: Claude PR agents @@ -87,13 +92,19 @@ concurrency: jobs: review: if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "codex-code-review" secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} context-files: + needs: review if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "context-files" + args: "auto ." secrets: ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} ``` @@ -109,58 +120,74 @@ In your target repo: **Settings β†’ Secrets and variables β†’ Actions β†’ New re ### Step 3 β€” Open a PR and verify -Open a draft PR, then mark it **Ready for review**. The workflow should appear in the **Actions** tab and Claude should post output to the PR within a minute or two. +Open a draft PR, then mark it **Ready for review**. The workflow appears in the **Actions** tab and Claude posts output to the PR within a minute or two. --- -## Customizing a prompt +## Customization options + +### Use a raw prompt (no skill file) -Every agent accepts a `prompt_override` input that replaces the default prompt entirely. Use this when you want different review criteria for a specific repo. +For a one-off instruction without creating a skill: ```yaml -jobs: - review: - if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/claude_pr_review.yml@v1 - secrets: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - with: - prompt_override: | - You are reviewing a security-sensitive payments service. - Flag any use of eval(), exec(), unsanitized SQL, or raw HTTP calls. - Produce a pass/fail verdict with severity ratings. +with: + prompt: | + Review this diff for security issues only. + Flag any use of eval(), exec(), unsanitized SQL, or raw HTTP calls. + Give a pass/fail verdict. +``` + +### Use a local skill from the target repo + +Target repos can define their own `SKILL.md` files and pass the path: + +```yaml +with: + skill: ".claude/skills/team-conventions" # .claude/skills/team-conventions/SKILL.md + # OR + skill: "./skills/security-review" # skills/security-review/SKILL.md + # OR + skill: "./prompts/quick-check.md" # flat .md file, no directory ``` -The context files agent additionally accepts `agent_args` to control mode and scope without needing a full override: +This follows the Claude Code skill convention: paths containing `/` are resolved from the target repo checkout; bare names are resolved from the central repo's `skills/` directory. + +### Mix central and local skills ```yaml jobs: - context-files: - if: ${{ github.event.pull_request.draft == false }} - uses: safurrier/python-collab-template/.github/workflows/context_files_agent.yml@v1 - secrets: - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + review: + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 + with: + skill: "codex-code-review" # central repo skill + secrets: { ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} } + + conventions: + needs: review + uses: safurrier/python-collab-template/.github/workflows/claude_pr_agent.yml@v1 with: - agent_args: "auto src/" # scope to src/ instead of repo root + skill: ".claude/skills/api-conventions" # target repo local skill + secrets: { ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} } ``` --- ## Trigger policy reference -| Event | Review agent | Context files agent | +| Event | Review skill | Context files skill | |---|---|---| | PR opened as draft | Skipped | Skipped | | Draft PR gets new commits | Skipped | Skipped | | PR marked Ready for review | Runs | Runs | | New commits pushed to open PR | Runs | Not triggered | -| Closed PR reopened | Runs (if not draft) | Not triggered | +| Closed PR reopened (non-draft) | Runs | Not triggered | | PR first opened (non-draft) | Not triggered | Runs | --- ## Private vs public repos -Use `pull_request` (not `pull_request_target`) β€” this is the correct event for repos without forks. The `ANTHROPIC_API_KEY` secret is accessible to `pull_request` workflows run from branches in the same repo. +Use `pull_request` (not `pull_request_target`) β€” correct for repos without forks. The `ANTHROPIC_API_KEY` secret is accessible to `pull_request` workflows from branches in the same repo. -If your central workflow repo (this one) is **private**, target repos must be granted access: **Settings β†’ Actions β†’ Access β†’ Accessible from repositories in your account**. +If this central workflow repo is **private**, target repos need access: **Settings β†’ Actions β†’ Access β†’ Accessible from repositories in your account**. diff --git a/prompts/codex_code_review_prompt.md b/skills/codex-code-review/SKILL.md similarity index 75% rename from prompts/codex_code_review_prompt.md rename to skills/codex-code-review/SKILL.md index f5a1969..73b1ad5 100644 --- a/prompts/codex_code_review_prompt.md +++ b/skills/codex-code-review/SKILL.md @@ -1 +1,8 @@ +--- +name: codex-code-review +description: Reviews PR diffs using the official Codex Code Review prompt. Posts actionable findings by category and a correctness verdict with confidence score. +argument-hint: "" +allowed-tools: Read, Grep, Glob, Bash +--- + You are acting as a reviewer for a proposed code change made by another engineer. Focus on issues that impact correctness, performance, security, maintainability, or developer experience. Flag only actionable issues introduced by the pull request. When you flag an issue, provide a short, direct explanation and cite the affected file and line range. Prioritize severe issues and avoid nit-level comments unless they block understanding of the diff. After listing findings, produce an overall correctness verdict ("patch is correct" or "patch is incorrect") with a concise justification and a confidence score between 0 and 1. Ensure that file citations and line numbers are exactly correct using the tools available; if they are incorrect your comments will be rejected. diff --git a/prompts/context_files_prompt.md b/skills/context-files/SKILL.md similarity index 98% rename from prompts/context_files_prompt.md rename to skills/context-files/SKILL.md index 4bac298..fefca43 100644 --- a/prompts/context_files_prompt.md +++ b/skills/context-files/SKILL.md @@ -1,6 +1,8 @@ --- -name: ai-agent-context-files -description: Create, update, or evaluate AGENTS.md and CLAUDE.md files for a repository. This skill activates when users want to create onboarding docs, update agent context files, or evaluate existing documentation quality. Follows WHY/WHAT/HOW structure with validated commands and progressive disclosure. +name: context-files +description: Create, update, or evaluate AGENTS.md and CLAUDE.md files for a repository. Follows WHY/WHAT/HOW structure with validated commands and progressive disclosure. +argument-hint: "[mode] [scope]" +allowed-tools: Read, Write, Edit, Bash, Grep, Glob --- You are operating inside a real codebase. Your job is to produce and maintain **high-signal agent onboarding memory files**: