diff --git a/.env.example b/.env.example index 27cddd2..e82d185 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,28 @@ # API Keys (Required to enable respective provider) ANTHROPIC_API_KEY="your_anthropic_api_key_here" # Required: Format: sk-ant-api03-... -PERPLEXITY_API_KEY="your_perplexity_api_key_here" # Optional: Format: pplx-... -OPENAI_API_KEY="your_openai_api_key_here" # Optional, for OpenAI/OpenRouter models. Format: sk-proj-... -GOOGLE_API_KEY="your_google_api_key_here" # Optional, for Google Gemini models. -MISTRAL_API_KEY="your_mistral_key_here" # Optional, for Mistral AI models. -XAI_API_KEY="YOUR_XAI_KEY_HERE" # Optional, for xAI AI models. -AZURE_OPENAI_API_KEY="your_azure_key_here" # Optional, for Azure OpenAI models (requires endpoint in .taskmaster/config.json). -OLLAMA_API_KEY="your_ollama_api_key_here" # Optional: For remote Ollama servers that require authentication. + +# Test repository (format: owner/repo) +TOADY_TEST_REPO=tonyblank/toady-integration-tests + +# Test organization +TOADY_TEST_ORG=tonyblank + +# Test PR number (PR that has review comments) +TOADY_TEST_PR_NUMBER=1 + +# API timeout in seconds +TOADY_API_TIMEOUT=30 + +# Rate limit buffer (minimum remaining API calls) +TOADY_RATE_LIMIT_BUFFER=100 + +# Skip slow tests (true/false) +TOADY_SKIP_SLOW_TESTS=false + +# Retry configuration +TOADY_MAX_RETRY_ATTEMPTS=3 +TOADY_RETRY_DELAY=1.0 + +# GitHub token (optional - gh CLI should handle auth) +# GH_TOKEN=your_github_token_here +EOF < /dev/null diff --git a/.github/workflows/ci-uv.yml b/.github/workflows/ci-uv.yml new file mode 100644 index 0000000..41c5724 --- /dev/null +++ b/.github/workflows/ci-uv.yml @@ -0,0 +1,44 @@ +name: CI with uv + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v4 + with: + version: "latest" + + - name: Set up Python ${{ matrix.python-version }} + run: uv python install ${{ matrix.python-version }} + + - name: Install dependencies + run: | + uv sync --all-extras + + - name: Run linting + run: | + uv run ruff check src tests + uv run black --check src tests + uv run mypy src + + - name: Run tests + run: | + uv run pytest --cov=src/toady --cov-report=xml + + - name: Upload coverage + uses: codecov/codecov-action@v5 + with: + files: ./coverage.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0d57a23..79346a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,14 +19,14 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-lint-${{ hashFiles('requirements-dev.txt') }} + key: ${{ runner.os }}-pip-lint-${{ hashFiles('pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip-lint- ${{ runner.os }}-pip- - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install -e ".[dev]" - name: Run pre-commit run: pre-commit run --all-files @@ -36,7 +36,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v4 @@ -48,7 +48,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-test-${{ matrix.python-version }}-${{ hashFiles('requirements-dev.txt') }} + key: ${{ runner.os }}-pip-test-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip-test-${{ matrix.python-version }}- ${{ runner.os }}-pip-test- @@ -56,11 +56,10 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-dev.txt - pip install -e . + pip install -e ".[dev]" - name: Run tests with pytest run: | - pytest -v --cov=toady --cov-report=xml:coverage.xml --cov-report=term-missing + pytest -v --cov=toady --cov-report=xml:coverage.xml --cov-report=term-missing -m "not real_api" - name: Upload coverage to Codecov if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.11' uses: codecov/codecov-action@v5 @@ -68,6 +67,41 @@ jobs: files: ./coverage.xml fail_ci_if_error: false + integration-test: + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.TOADY_INTEGRATION_TOKEN || secrets.GITHUB_TOKEN }} + TOADY_TEST_REPO: tonyblank/toady-integration-tests + TOADY_TEST_ORG: tonyblank + TOADY_TEST_PR_NUMBER: 1 + TOADY_API_TIMEOUT: 30 + TOADY_RATE_LIMIT_BUFFER: 100 + TOADY_SKIP_SLOW_TESTS: false + TOADY_MAX_RETRY_ATTEMPTS: 3 + TOADY_RETRY_DELAY: 1.0 + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Cache pip packages + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-integration-${{ hashFiles('pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip-integration- + ${{ runner.os }}-pip- + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + - name: Run integration tests + run: | + pytest -v tests/integration/ -m "real_api" --tb=short --maxfail=3 + continue-on-error: true + build: runs-on: ubuntu-latest needs: [lint, test] diff --git a/Makefile b/Makefile index bc2d005..d4d14b0 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,21 @@ -.PHONY: help install install-dev test test-fast test-integration test-performance test-analysis lint format format-check type-check pre-commit check check-fast fix-check clean build +.PHONY: help install install-dev test test-fast test-integration test-performance test-analysis +.PHONY: lint format format-check type-check pre-commit check check-fast fix-check clean build +.PHONY: sync lock update add remove deps-check shell run + +# Single source of truth: ALL commands use uv +export PATH := $(HOME)/.local/bin:$(PATH) # Default target help: - @echo "Available commands:" + @echo "๐Ÿ Toady CLI Development Commands (Powered by uv)" @echo "" @echo "๐Ÿš€ Installation:" @echo " make install Install package in production mode" @echo " make install-dev Install package in development mode with all dev dependencies" + @echo " make sync Sync dependencies from lock file (fastest)" @echo "" - @echo "๐Ÿงช Testing:" - @echo " make test Run all tests with coverage (80% threshold)" + @echo "๐Ÿงช Testing (Advanced Test Suite):" + @echo " make test Run all tests with coverage (90% threshold)" @echo " make test-fast Run fast unit tests only" @echo " make test-integration Run integration tests only" @echo " make test-performance Run performance benchmarks" @@ -22,72 +28,88 @@ help: @echo " make type-check Run type checking with mypy" @echo " make pre-commit Run all pre-commit hooks" @echo "" - @echo "โœ… CI/CD Pipeline:" + @echo "โœ… CI/CD Pipeline (Elegant Reporting):" @echo " make check ๐ŸŽฏ Run COMPREHENSIVE CI/CD pipeline (all checks + tests)" @echo " make check-fast โšก Run FAST quality checks (no tests, quick validation)" @echo " make fix-check ๐Ÿ”ง Run checks with auto-fixing (like CI pipeline)" @echo "" + @echo "๐Ÿ“ฆ Dependency Management:" + @echo " make lock Generate/update lock file with exact versions" + @echo " make update Update dependencies to latest compatible versions" + @echo " make add PKG=name Add new dependency" + @echo " make remove PKG=name Remove dependency" + @echo " make deps-check Check for dependency conflicts" + @echo "" + @echo "๐Ÿ› ๏ธ Development Utilities:" + @echo " make shell Open shell with project dependencies loaded" + @echo " make run ARGS='...' Run toady CLI in development mode" + @echo "" @echo "๐Ÿงน Maintenance:" @echo " make clean Remove build artifacts and cache files" @echo " make build Build distribution packages" install: - pip install -e . + @echo "๐Ÿ“ฆ Installing package in production mode..." + uv pip install . install-dev: - pip install -e ".[dev]" - pre-commit install + @echo "๐Ÿ”ง Installing development environment..." + uv sync --all-extras + uv run pre-commit install + @echo "โœ… Development environment ready!" +## Testing (Advanced Test Suite - ALL using uv) test: - @echo "๐Ÿงช Running comprehensive test suite with 80% coverage requirement..." - python3 scripts/test_config.py full - + @echo "๐Ÿงช Running comprehensive test suite with 90% coverage requirement..." + uv run python scripts/test_config.py full test-fast: @echo "โšก Running fast unit tests..." - python3 scripts/test_config.py fast + uv run python scripts/test_config.py fast test-integration: @echo "๐Ÿ”— Running integration tests..." - python3 scripts/test_config.py integration + uv run python scripts/test_config.py integration test-performance: @echo "๐Ÿ“Š Running performance benchmarks..." - python3 scripts/test_config.py performance + uv run python scripts/test_config.py performance test-analysis: @echo "๐Ÿ“ˆ Generating test suite analysis..." - python3 scripts/test_config.py analyze - python3 scripts/test_config.py report + uv run python scripts/test_config.py analyze + uv run python scripts/test_config.py report +## Code Quality (ALL using uv) lint: - ruff check --no-fix src tests + uv run ruff check --no-fix src tests format: - black src tests + uv run black src tests format-check: - black --check src tests + uv run black --check src tests type-check: - mypy --strict --ignore-missing-imports src + uv run mypy --strict --ignore-missing-imports src pre-commit: - pre-commit run --all-files + uv run pre-commit run --all-files +## CI/CD Pipeline (Elegant Reporting - ALL using uv) # ๐ŸŽฏ COMPREHENSIVE CI/CD PIPELINE # This is the main command that runs all quality checks, tests, and validations # with beautiful reporting and elegant progress tracking check: @echo "๐Ÿš€ Launching comprehensive CI/CD pipeline..." - python3 scripts/ci_check.py full + uv run python scripts/ci_check.py full # โšก FAST QUALITY CHECK PIPELINE # Quick validation without running the full test suite # Perfect for rapid development feedback check-fast: @echo "โšก Running fast quality check pipeline..." - python3 scripts/ci_check.py fast + uv run python scripts/ci_check.py fast # ๐Ÿ”ง AUTO-FIXING PIPELINE # Runs checks with automatic fixing where possible @@ -96,17 +118,53 @@ fix-check: @echo "๐Ÿ”ง Running auto-fixing CI/CD pipeline..." @echo "" @echo "๐Ÿ“‹ Step 1: Auto-fixing code issues..." - pre-commit run --all-files || true + uv run pre-commit run --all-files || true @echo "" @echo "๐Ÿ” Step 2: Running type checks..." - mypy --strict --ignore-missing-imports src + uv run mypy --strict --ignore-missing-imports src @echo "" @echo "๐Ÿงช Step 3: Running comprehensive tests..." - python3 scripts/test_config.py full + uv run python scripts/test_config.py full @echo "" @echo "โœจ Auto-fixing pipeline completed!" +## Dependency Management (uv native commands) +sync: + @echo "โšก Syncing dependencies from lock file..." + uv sync + +lock: + @echo "๐Ÿ”’ Generating lock file with exact versions..." + uv lock + +update: + @echo "โฌ†๏ธ Updating dependencies to latest compatible versions..." + uv lock --upgrade + +add: + @echo "โž• Adding dependency: $(PKG)" + uv add $(PKG) + +remove: + @echo "โž– Removing dependency: $(PKG)" + uv remove $(PKG) + +deps-check: + @echo "๐Ÿ” Checking for dependency conflicts..." + uv pip check + +## Development Utilities (uv powered) +shell: + @echo "๐Ÿš Opening shell with project dependencies..." + uv shell + +run: + @echo "๐Ÿš€ Running toady CLI in development mode..." + uv run toady $(ARGS) + +## Maintenance (Enhanced) clean: + @echo "๐Ÿงน Cleaning build artifacts and cache..." rm -rf build/ rm -rf dist/ rm -rf *.egg-info @@ -115,8 +173,10 @@ clean: rm -rf .pytest_cache/ rm -rf .mypy_cache/ rm -rf .ruff_cache/ + rm -rf test-reports/ find . -type d -name __pycache__ -exec rm -rf {} + find . -type f -name "*.pyc" -delete build: clean - python -m build + @echo "๐Ÿ“ฆ Building distribution packages..." + uv build diff --git a/coverage.json b/coverage.json new file mode 100644 index 0000000..b1187e6 --- /dev/null +++ b/coverage.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.8.2", "timestamp": "2025-06-12T22:21:38.266839", "branch_coverage": true, "show_contexts": false}, "files": {"src/toady/__init__.py": {"executed_lines": [1, 3, 4, 5], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1, 3, 4, 5], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 5], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/cli.py": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 22, 23, 88, 89, 93, 94, 95, 96, 99, 101, 102, 103, 105, 107, 108, 109, 111, 113, 114, 116, 119, 122], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 2, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [122, 123], "executed_branches": [[114, 116], [114, 119]], "missing_branches": [], "functions": {"cli": {"executed_lines": [88, 89], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "main": {"executed_lines": [101, 102, 103, 105, 107, 108, 109, 111, 113, 114, 116, 119], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[114, 116], [114, 119]], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 22, 23, 93, 94, 95, 96, 99, 122], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 2, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [122, 123], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 22, 23, 88, 89, 93, 94, 95, 96, 99, 101, 102, 103, 105, 107, 108, 109, 111, 113, 114, 116, 119, 122], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 2, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [122, 123], "executed_branches": [[114, 116], [114, 119]], "missing_branches": []}}}, "src/toady/command_utils.py": {"executed_lines": [1, 3, 4, 6, 8, 9, 12, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 39, 41, 43, 45, 48, 57, 58, 61, 62, 63, 70, 80, 81, 82, 83], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[34, 35], [34, 39], [57, 58], [57, 61], [62, -48], [62, 63], [80, 81], [80, 82], [82, -70], [82, 83]], "missing_branches": [], "functions": {"handle_command_errors": {"executed_lines": [25, 26, 45], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "handle_command_errors.wrapper": {"executed_lines": [27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 39, 41, 43], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[34, 35], [34, 39]], "missing_branches": []}, "validate_pr_number": {"executed_lines": [57, 58, 61, 62, 63], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[57, 58], [57, 61], [62, -48], [62, 63]], "missing_branches": []}, "validate_limit": {"executed_lines": [80, 81, 82, 83], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[80, 81], [80, 82], [82, -70], [82, 83]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 8, 9, 12, 48, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 8, 9, 12, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 39, 41, 43, 45, 48, 57, 58, 61, 62, 63, 70, 80, 81, 82, 83], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[34, 35], [34, 39], [57, 58], [57, 61], [62, -48], [62, 63], [80, 81], [80, 82], [82, -70], [82, 83]], "missing_branches": []}}}, "src/toady/commands/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/commands/fetch.py": {"executed_lines": [1, 3, 5, 7, 11, 17, 20, 21, 30, 31, 32, 38, 46, 47, 122, 123, 124, 127, 128, 134, 137, 138, 140, 141, 150, 151, 154, 163, 165, 166, 168, 169, 176, 179, 180, 181, 182, 183, 186, 193, 194, 195, 196, 197, 200, 201, 202, 203, 204, 205, 207, 209], "summary": {"covered_lines": 51, "num_statements": 54, "percent_covered": 95.71428571428571, "percent_covered_display": "95.71", "missing_lines": 3, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [129, 130, 131], "excluded_lines": [], "executed_branches": [[122, 123], [122, 124], [150, 151], [150, 154], [179, 180], [179, 186], [194, 195], [194, 200], [195, 194], [195, 196], [200, 201], [200, 209], [202, 203], [202, 204], [204, 205], [204, 207]], "missing_branches": [], "functions": {"fetch": {"executed_lines": [122, 123, 124, 127, 128, 134, 137, 138, 140, 141, 150, 151, 154, 163, 165, 166, 168, 169, 176, 179, 180, 181, 182, 183, 186, 193, 194, 195, 196, 197, 200, 201, 202, 203, 204, 205, 207, 209], "summary": {"covered_lines": 38, "num_statements": 41, "percent_covered": 94.73684210526316, "percent_covered_display": "94.74", "missing_lines": 3, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [129, 130, 131], "excluded_lines": [], "executed_branches": [[122, 123], [122, 124], [150, 151], [150, 154], [179, 180], [179, 186], [194, 195], [194, 200], [195, 194], [195, 196], [200, 201], [200, 209], [202, 203], [202, 204], [204, 205], [204, 207]], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 11, 17, 20, 21, 30, 31, 32, 38, 46, 47], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 5, 7, 11, 17, 20, 21, 30, 31, 32, 38, 46, 47, 122, 123, 124, 127, 128, 134, 137, 138, 140, 141, 150, 151, 154, 163, 165, 166, 168, 169, 176, 179, 180, 181, 182, 183, 186, 193, 194, 195, 196, 197, 200, 201, 202, 203, 204, 205, 207, 209], "summary": {"covered_lines": 51, "num_statements": 54, "percent_covered": 95.71428571428571, "percent_covered_display": "95.71", "missing_lines": 3, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [129, 130, 131], "excluded_lines": [], "executed_branches": [[122, 123], [122, 124], [150, 151], [150, 154], [179, 180], [179, 186], [194, 195], [194, 200], [195, 194], [195, 196], [200, 201], [200, 209], [202, 203], [202, 204], [204, 205], [204, 207]], "missing_branches": []}}}, "src/toady/commands/reply.py": {"executed_lines": [1, 3, 4, 6, 8, 14, 20, 25, 28, 34, 91, 92, 95, 111, 112, 113, 115, 118, 119, 122, 123, 134, 136, 137, 140, 141, 153, 157, 160, 174, 177, 178, 179, 181, 182, 188, 189, 194, 195, 202, 203, 208, 211, 218, 221, 223, 224, 225, 226, 227, 228, 231, 232, 233, 234, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 250, 264, 275, 284, 285, 286, 289, 290, 292, 295, 302, 303, 306, 307, 313, 314, 320, 328, 329, 330, 333, 344, 385, 386, 387, 388, 389, 390, 391, 393, 400, 401, 404, 405, 406, 407, 408, 409, 412, 415, 419, 426, 428, 429, 430, 431, 432, 444, 447, 448, 449, 450, 451, 453, 460, 461, 464, 465, 475, 482, 483, 484, 491, 496, 497, 575, 576, 580, 581, 582, 583, 586, 587, 588, 589, 590, 593, 596, 599, 602, 603, 604, 606, 608, 609, 611, 612], "summary": {"covered_lines": 151, "num_statements": 156, "percent_covered": 94.39655172413794, "percent_covered_display": "94.40", "missing_lines": 5, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 8, "covered_branches": 68, "missing_branches": 8}, "missing_lines": [436, 443, 577, 614, 615], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [122, 123], [122, 134], [140, 141], [140, 153], [178, 179], [178, 181], [181, 182], [181, 188], [188, 189], [188, 194], [194, 195], [194, 202], [202, 203], [202, 208], [221, 223], [221, 227], [224, 225], [227, 228], [227, 231], [231, -211], [231, 232], [233, 234], [238, 239], [240, 241], [242, 243], [244, 245], [246, 247], [284, 285], [284, 289], [285, 284], [285, 286], [289, 290], [289, 292], [302, 303], [302, 306], [306, 307], [306, 313], [313, -295], [313, 314], [328, -320], [328, 329], [385, 386], [385, 404], [386, 385], [386, 387], [387, 388], [387, 393], [390, 391], [390, 401], [404, 405], [404, 447], [405, 406], [405, 428], [406, 407], [406, 419], [428, 429], [447, 448], [447, 453], [575, 576], [575, 580], [580, 581], [580, 582], [582, 583], [582, 586], [608, 609], [608, 611]], "missing_branches": [[224, 226], [233, 238], [238, 240], [240, 242], [242, 244], [244, 246], [246, -211], [428, 436]], "functions": {"_show_id_help": {"executed_lines": [34, 91, 92], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_reply_target_id": {"executed_lines": [111, 112, 113, 115, 118, 119, 122, 123, 134, 136, 137, 140, 141, 153, 157], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [122, 123], [122, 134], [140, 141], [140, 153]], "missing_branches": []}, "_validate_reply_args": {"executed_lines": [174, 177, 178, 179, 181, 182, 188, 189, 194, 195, 202, 203, 208], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[178, 179], [178, 181], [181, 182], [181, 188], [188, 189], [188, 194], [194, 195], [194, 202], [202, 203], [202, 208]], "missing_branches": []}, "_print_pretty_reply": {"executed_lines": [218, 221, 223, 224, 225, 226, 227, 228, 231, 232, 233, 234, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 7, "covered_branches": 13, "missing_branches": 7}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[221, 223], [221, 227], [224, 225], [227, 228], [227, 231], [231, -211], [231, 232], [233, 234], [238, 239], [240, 241], [242, 243], [244, 245], [246, 247]], "missing_branches": [[224, 226], [233, 238], [238, 240], [240, 242], [242, 244], [244, 246], [246, -211]]}, "_build_json_reply": {"executed_lines": [264, 275, 284, 285, 286, 289, 290, 292], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[284, 285], [284, 289], [285, 284], [285, 286], [289, 290], [289, 292]], "missing_branches": []}, "_show_warnings": {"executed_lines": [302, 303, 306, 307, 313, 314], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[302, 303], [302, 306], [306, 307], [306, 313], [313, -295], [313, 314]], "missing_branches": []}, "_show_progress": {"executed_lines": [328, 329, 330], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[328, -320], [328, 329]], "missing_branches": []}, "_handle_reply_error": {"executed_lines": [344, 385, 386, 387, 388, 389, 390, 391, 393, 400, 401, 404, 405, 406, 407, 408, 409, 412, 415, 419, 426, 428, 429, 430, 431, 432, 444, 447, 448, 449, 450, 451, 453, 460, 461], "summary": {"covered_lines": 35, "num_statements": 37, "percent_covered": 94.54545454545455, "percent_covered_display": "94.55", "missing_lines": 2, "excluded_lines": 0, "num_branches": 18, "num_partial_branches": 1, "covered_branches": 17, "missing_branches": 1}, "missing_lines": [436, 443], "excluded_lines": [], "executed_branches": [[385, 386], [385, 404], [386, 385], [386, 387], [387, 388], [387, 393], [390, 391], [390, 401], [404, 405], [404, 447], [405, 406], [405, 428], [406, 407], [406, 419], [428, 429], [447, 448], [447, 453]], "missing_branches": [[428, 436]]}, "reply": {"executed_lines": [575, 576, 580, 581, 582, 583, 586, 587, 588, 589, 590, 593, 596, 599, 602, 603, 604, 606, 608, 609, 611, 612], "summary": {"covered_lines": 22, "num_statements": 25, "percent_covered": 90.9090909090909, "percent_covered_display": "90.91", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [577, 614, 615], "excluded_lines": [], "executed_branches": [[575, 576], [575, 580], [580, 581], [580, 582], [582, 583], [582, 586], [608, 609], [608, 611]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 8, 14, 20, 25, 28, 95, 160, 211, 250, 295, 320, 333, 464, 465, 475, 482, 483, 484, 491, 496, 497], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 8, 14, 20, 25, 28, 34, 91, 92, 95, 111, 112, 113, 115, 118, 119, 122, 123, 134, 136, 137, 140, 141, 153, 157, 160, 174, 177, 178, 179, 181, 182, 188, 189, 194, 195, 202, 203, 208, 211, 218, 221, 223, 224, 225, 226, 227, 228, 231, 232, 233, 234, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 250, 264, 275, 284, 285, 286, 289, 290, 292, 295, 302, 303, 306, 307, 313, 314, 320, 328, 329, 330, 333, 344, 385, 386, 387, 388, 389, 390, 391, 393, 400, 401, 404, 405, 406, 407, 408, 409, 412, 415, 419, 426, 428, 429, 430, 431, 432, 444, 447, 448, 449, 450, 451, 453, 460, 461, 464, 465, 475, 482, 483, 484, 491, 496, 497, 575, 576, 580, 581, 582, 583, 586, 587, 588, 589, 590, 593, 596, 599, 602, 603, 604, 606, 608, 609, 611, 612], "summary": {"covered_lines": 151, "num_statements": 156, "percent_covered": 94.39655172413794, "percent_covered_display": "94.40", "missing_lines": 5, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 8, "covered_branches": 68, "missing_branches": 8}, "missing_lines": [436, 443, 577, 614, 615], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [122, 123], [122, 134], [140, 141], [140, 153], [178, 179], [178, 181], [181, 182], [181, 188], [188, 189], [188, 194], [194, 195], [194, 202], [202, 203], [202, 208], [221, 223], [221, 227], [224, 225], [227, 228], [227, 231], [231, -211], [231, 232], [233, 234], [238, 239], [240, 241], [242, 243], [244, 245], [246, 247], [284, 285], [284, 289], [285, 284], [285, 286], [289, 290], [289, 292], [302, 303], [302, 306], [306, 307], [306, 313], [313, -295], [313, 314], [328, -320], [328, 329], [385, 386], [385, 404], [386, 385], [386, 387], [387, 388], [387, 393], [390, 391], [390, 401], [404, 405], [404, 447], [405, 406], [405, 428], [406, 407], [406, 419], [428, 429], [447, 448], [447, 453], [575, 576], [575, 580], [580, 581], [580, 582], [582, 583], [582, 586], [608, 609], [608, 611]], "missing_branches": [[224, 226], [233, 238], [238, 240], [240, 242], [242, 244], [244, 246], [246, -211], [428, 436]]}}}, "src/toady/commands/resolve.py": {"executed_lines": [1, 3, 4, 5, 7, 9, 10, 18, 23, 24, 25, 28, 42, 43, 45, 47, 48, 55, 56, 58, 60, 63, 83, 84, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 101, 106, 109, 128, 129, 133, 134, 135, 136, 138, 139, 140, 145, 146, 147, 149, 150, 153, 154, 156, 157, 158, 159, 160, 161, 165, 166, 167, 168, 169, 172, 175, 197, 198, 199, 200, 201, 202, 203, 204, 205, 207, 216, 219, 228, 229, 231, 234, 245, 246, 247, 249, 258, 261, 277, 290, 291, 292, 293, 294, 295, 296, 298, 305, 306, 309, 310, 313, 314, 316, 323, 324, 327, 340, 342, 344, 347, 348, 349, 352, 357, 362, 374, 375, 377, 378, 381, 396, 397, 401, 402, 405, 406, 409, 410, 413, 415, 418, 430, 431, 432, 434, 435, 436, 437, 439, 442, 450, 451, 452, 453, 456, 466, 467, 468, 469, 470, 472, 475, 491, 493, 511, 512, 513, 514, 515, 516, 517, 519, 526, 527, 530, 531, 532, 534, 541, 542, 545, 557, 560, 563, 564, 566, 567, 569, 571, 573, 574, 577, 578, 585, 592, 600, 606, 612, 613, 614, 621, 622, 718, 719, 720, 721, 722, 726, 729, 732, 733, 734, 735, 738, 741], "summary": {"covered_lines": 205, "num_statements": 207, "percent_covered": 98.29351535836177, "percent_covered_display": "98.29", "missing_lines": 2, "excluded_lines": 0, "num_branches": 86, "num_partial_branches": 3, "covered_branches": 83, "missing_branches": 3}, "missing_lines": [170, 737], "excluded_lines": [], "executed_branches": [[42, 43], [42, 45], [55, 56], [55, 58], [83, 84], [83, 86], [86, 87], [86, 101], [91, 92], [91, 93], [93, 94], [93, 95], [96, -63], [96, 97], [128, 129], [128, 133], [138, 139], [138, 172], [139, 140], [139, 145], [146, 147], [146, 149], [153, 138], [153, 154], [159, 160], [169, 138], [197, 198], [197, 207], [201, -175], [201, 202], [204, -175], [204, 205], [228, 229], [228, 231], [245, 246], [245, 249], [290, 291], [290, 309], [291, 290], [291, 292], [292, 293], [292, 298], [295, 296], [295, 306], [309, 310], [309, 313], [313, 314], [313, 316], [347, 348], [347, 352], [374, -327], [374, 375], [396, 397], [396, 401], [401, 402], [401, 405], [405, 406], [405, 409], [409, 410], [409, 413], [431, 432], [431, 434], [450, -442], [450, 451], [466, 467], [466, 472], [469, -456], [469, 470], [511, 512], [511, 530], [512, 511], [512, 513], [513, 514], [513, 519], [516, 517], [516, 527], [530, 531], [531, 532], [531, 534], [566, 567], [566, 569], [732, 733], [732, 741]], "missing_branches": [[159, 165], [169, 170], [530, -475]], "functions": {"_fetch_and_filter_threads": {"executed_lines": [42, 43, 45, 47, 48, 55, 56, 58, 60], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[42, 43], [42, 45], [55, 56], [55, 58]], "missing_branches": []}, "_handle_confirmation_prompt": {"executed_lines": [83, 84, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 101, 106], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[83, 84], [83, 86], [86, 87], [86, 101], [91, 92], [91, 93], [93, 94], [93, 95], [96, -63], [96, 97]], "missing_branches": []}, "_process_threads": {"executed_lines": [128, 129, 133, 134, 135, 136, 138, 139, 140, 145, 146, 147, 149, 150, 153, 154, 156, 157, 158, 159, 160, 161, 165, 166, 167, 168, 169, 172], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 93.02325581395348, "percent_covered_display": "93.02", "missing_lines": 1, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 2, "covered_branches": 12, "missing_branches": 2}, "missing_lines": [170], "excluded_lines": [], "executed_branches": [[128, 129], [128, 133], [138, 139], [138, 172], [139, 140], [139, 145], [146, 147], [146, 149], [153, 138], [153, 154], [159, 160], [169, 138]], "missing_branches": [[159, 165], [169, 170]]}, "_display_summary": {"executed_lines": [197, 198, 199, 200, 201, 202, 203, 204, 205, 207, 216], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[197, 198], [197, 207], [201, -175], [201, 202], [204, -175], [204, 205]], "missing_branches": []}, "_get_action_labels": {"executed_lines": [228, 229, 231], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[228, 229], [228, 231]], "missing_branches": []}, "_handle_empty_threads": {"executed_lines": [245, 246, 247, 249, 258], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[245, 246], [245, 249]], "missing_branches": []}, "_handle_bulk_resolve_error": {"executed_lines": [277, 290, 291, 292, 293, 294, 295, 296, 298, 305, 306, 309, 310, 313, 314, 316, 323, 324], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[290, 291], [290, 309], [291, 290], [291, 292], [292, 293], [292, 298], [295, 296], [295, 306], [309, 310], [309, 313], [313, 314], [313, 316]], "missing_branches": []}, "_handle_bulk_resolve": {"executed_lines": [340, 342, 344, 347, 348, 349, 352, 357, 362, 374, 375, 377, 378], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[347, 348], [347, 352], [374, -327], [374, 375]], "missing_branches": []}, "_validate_resolve_parameters": {"executed_lines": [396, 397, 401, 402, 405, 406, 409, 410, 413, 415], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[396, 397], [396, 401], [401, 402], [401, 405], [405, 406], [405, 409], [409, 410], [409, 413]], "missing_branches": []}, "_validate_and_prepare_thread_id": {"executed_lines": [430, 431, 432, 434, 435, 436, 437, 439], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[431, 432], [431, 434]], "missing_branches": []}, "_show_single_resolve_progress": {"executed_lines": [450, 451, 452, 453], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[450, -442], [450, 451]], "missing_branches": []}, "_handle_single_resolve_success": {"executed_lines": [466, 467, 468, 469, 470, 472], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[466, 467], [466, 472], [469, -456], [469, 470]], "missing_branches": []}, "_handle_single_resolve_error": {"executed_lines": [491, 493, 511, 512, 513, 514, 515, 516, 517, 519, 526, 527, 530, 531, 532, 534, 541, 542], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.66666666666667, "percent_covered_display": "96.67", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 1, "covered_branches": 11, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[511, 512], [511, 530], [512, 511], [512, 513], [513, 514], [513, 519], [516, 517], [516, 527], [530, 531], [531, 532], [531, 534]], "missing_branches": [[530, -475]]}, "_handle_single_resolve": {"executed_lines": [557, 560, 563, 564, 566, 567, 569, 571, 573, 574], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[566, 567], [566, 569]], "missing_branches": []}, "resolve": {"executed_lines": [718, 719, 720, 721, 722, 726, 729, 732, 733, 734, 735, 738, 741], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 93.75, "percent_covered_display": "93.75", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [737], "excluded_lines": [], "executed_branches": [[732, 733], [732, 741]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 9, 10, 18, 23, 24, 25, 28, 63, 109, 175, 219, 234, 261, 327, 381, 418, 442, 456, 475, 545, 577, 578, 585, 592, 600, 606, 612, 613, 614, 621, 622], "summary": {"covered_lines": 35, "num_statements": 35, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 9, 10, 18, 23, 24, 25, 28, 42, 43, 45, 47, 48, 55, 56, 58, 60, 63, 83, 84, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 101, 106, 109, 128, 129, 133, 134, 135, 136, 138, 139, 140, 145, 146, 147, 149, 150, 153, 154, 156, 157, 158, 159, 160, 161, 165, 166, 167, 168, 169, 172, 175, 197, 198, 199, 200, 201, 202, 203, 204, 205, 207, 216, 219, 228, 229, 231, 234, 245, 246, 247, 249, 258, 261, 277, 290, 291, 292, 293, 294, 295, 296, 298, 305, 306, 309, 310, 313, 314, 316, 323, 324, 327, 340, 342, 344, 347, 348, 349, 352, 357, 362, 374, 375, 377, 378, 381, 396, 397, 401, 402, 405, 406, 409, 410, 413, 415, 418, 430, 431, 432, 434, 435, 436, 437, 439, 442, 450, 451, 452, 453, 456, 466, 467, 468, 469, 470, 472, 475, 491, 493, 511, 512, 513, 514, 515, 516, 517, 519, 526, 527, 530, 531, 532, 534, 541, 542, 545, 557, 560, 563, 564, 566, 567, 569, 571, 573, 574, 577, 578, 585, 592, 600, 606, 612, 613, 614, 621, 622, 718, 719, 720, 721, 722, 726, 729, 732, 733, 734, 735, 738, 741], "summary": {"covered_lines": 205, "num_statements": 207, "percent_covered": 98.29351535836177, "percent_covered_display": "98.29", "missing_lines": 2, "excluded_lines": 0, "num_branches": 86, "num_partial_branches": 3, "covered_branches": 83, "missing_branches": 3}, "missing_lines": [170, 737], "excluded_lines": [], "executed_branches": [[42, 43], [42, 45], [55, 56], [55, 58], [83, 84], [83, 86], [86, 87], [86, 101], [91, 92], [91, 93], [93, 94], [93, 95], [96, -63], [96, 97], [128, 129], [128, 133], [138, 139], [138, 172], [139, 140], [139, 145], [146, 147], [146, 149], [153, 138], [153, 154], [159, 160], [169, 138], [197, 198], [197, 207], [201, -175], [201, 202], [204, -175], [204, 205], [228, 229], [228, 231], [245, 246], [245, 249], [290, 291], [290, 309], [291, 290], [291, 292], [292, 293], [292, 298], [295, 296], [295, 306], [309, 310], [309, 313], [313, 314], [313, 316], [347, 348], [347, 352], [374, -327], [374, 375], [396, 397], [396, 401], [401, 402], [401, 405], [405, 406], [405, 409], [409, 410], [409, 413], [431, 432], [431, 434], [450, -442], [450, 451], [466, 467], [466, 472], [469, -456], [469, 470], [511, 512], [511, 530], [512, 511], [512, 513], [513, 514], [513, 519], [516, 517], [516, 527], [530, 531], [531, 532], [531, 534], [566, 567], [566, 569], [732, 733], [732, 741]], "missing_branches": [[159, 165], [169, 170], [530, -475]]}}}, "src/toady/commands/schema.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 10, 17, 23, 24, 25, 27, 28, 31, 32, 37, 42, 48, 50, 52, 61, 62, 63, 64, 69, 70, 75, 76, 77, 78, 79, 83, 84, 91, 92, 93, 94, 95, 100, 101, 102, 104, 105, 106, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 124, 125, 126, 128, 129, 133, 134, 135, 136, 137, 138, 141, 142, 147, 152, 154, 156, 165, 166, 167, 168, 173, 174, 179, 180, 181, 182, 183, 187, 188, 195, 196, 197, 198, 199, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 214, 215, 219, 220, 221, 222, 223, 224, 227, 228, 229, 234, 240, 242, 244, 245, 252, 261, 262, 263, 264, 269, 270, 275, 276, 277, 278, 279, 285, 286, 287, 289, 290, 291, 296, 297, 298, 299, 304, 307, 309, 310, 314, 315, 316, 317, 322, 328, 329, 330, 335, 336, 337, 340, 341, 343, 344, 346, 347, 357, 358, 359, 360, 362, 363, 364, 366, 367, 368, 369, 370, 371, 376, 377, 378, 379, 381, 382, 383, 385, 386, 387, 388, 395, 396, 397, 398, 399, 401, 404, 410, 411, 412, 417, 418, 419, 422, 423, 425, 426, 428, 430, 431, 432, 435, 437, 438, 445, 446, 447, 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459, 462, 468, 469, 470, 476, 477, 478, 479, 480, 481, 485, 488, 489, 490, 491, 492, 493, 497, 499, 501, 502], "summary": {"covered_lines": 240, "num_statements": 261, "percent_covered": 89.21832884097034, "percent_covered_display": "89.22", "missing_lines": 21, "excluded_lines": 0, "num_branches": 110, "num_partial_branches": 11, "covered_branches": 91, "missing_branches": 19}, "missing_lines": [53, 130, 131, 132, 157, 216, 217, 218, 253, 300, 305, 306, 311, 312, 313, 318, 319, 373, 389, 390, 392], "excluded_lines": [], "executed_branches": [[27, -23], [27, 28], [52, 61], [101, 102], [101, 104], [121, 122], [121, 125], [123, 124], [123, 125], [129, 133], [133, 134], [133, 135], [156, 165], [203, 204], [215, 219], [219, 220], [219, 221], [244, 245], [244, 252], [252, 261], [286, 287], [286, 289], [310, 314], [314, 315], [314, 316], [329, 330], [329, 335], [359, 360], [359, 362], [362, 363], [362, 376], [366, 367], [366, 370], [368, 362], [368, 369], [370, 371], [378, 379], [378, 381], [381, 382], [381, 395], [385, 386], [387, 381], [387, 388], [396, 397], [396, 401], [398, 399], [398, 401], [411, 412], [411, 417], [417, 418], [417, 422], [425, 426], [425, 445], [426, 428], [426, 430], [431, 432], [431, 435], [445, 446], [445, 454], [447, 448], [447, 454], [449, 450], [449, 451], [451, 447], [451, 452], [454, -404], [454, 455], [456, -404], [456, 457], [458, 456], [458, 459], [469, 470], [469, 476], [477, 478], [477, 488], [478, 479], [478, 488], [479, 478], [479, 480], [480, 478], [480, 481], [481, 480], [481, 485], [489, 490], [490, 491], [490, 499], [491, 492], [492, 490], [492, 493], [493, 492], [493, 497]], "missing_branches": [[52, 53], [129, 130], [131, 132], [131, 133], [156, 157], [203, -141], [215, 216], [217, 218], [217, 219], [252, 253], [310, 311], [312, 313], [312, 314], [370, 373], [385, 389], [389, 390], [389, 392], [489, 499], [491, 490]], "functions": {"schema": {"executed_lines": [27, 28], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[27, -23], [27, 28]], "missing_branches": []}, "validate": {"executed_lines": [50, 52, 61, 62, 63, 64, 69, 70, 75, 76, 77, 78, 79, 83, 84, 91, 92, 93, 94, 95, 100, 101, 102, 104, 105, 106, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 124, 125, 126, 128, 129, 133, 134, 135, 136, 137, 138], "summary": {"covered_lines": 47, "num_statements": 51, "percent_covered": 87.6923076923077, "percent_covered_display": "87.69", "missing_lines": 4, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 4}, "missing_lines": [53, 130, 131, 132], "excluded_lines": [], "executed_branches": [[52, 61], [101, 102], [101, 104], [121, 122], [121, 125], [123, 124], [123, 125], [129, 133], [133, 134], [133, 135]], "missing_branches": [[52, 53], [129, 130], [131, 132], [131, 133]]}, "fetch": {"executed_lines": [154, 156, 165, 166, 167, 168, 173, 174, 179, 180, 181, 182, 183, 187, 188, 195, 196, 197, 198, 199, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 214, 215, 219, 220, 221, 222, 223, 224], "summary": {"covered_lines": 38, "num_statements": 42, "percent_covered": 82.6923076923077, "percent_covered_display": "82.69", "missing_lines": 4, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 3, "covered_branches": 5, "missing_branches": 5}, "missing_lines": [157, 216, 217, 218], "excluded_lines": [], "executed_branches": [[156, 165], [203, 204], [215, 219], [219, 220], [219, 221]], "missing_branches": [[156, 157], [203, -141], [215, 216], [217, 218], [217, 219]]}, "check": {"executed_lines": [242, 244, 245, 252, 261, 262, 263, 264, 269, 270, 275, 276, 277, 278, 279, 285, 286, 287, 289, 290, 291, 296, 297, 298, 299, 304, 307, 309, 310, 314, 315, 316, 317], "summary": {"covered_lines": 33, "num_statements": 42, "percent_covered": 75.92592592592592, "percent_covered_display": "75.93", "missing_lines": 9, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 4}, "missing_lines": [253, 300, 305, 306, 311, 312, 313, 318, 319], "excluded_lines": [], "executed_branches": [[244, 245], [244, 252], [252, 261], [286, 287], [286, 289], [310, 314], [314, 315], [314, 316]], "missing_branches": [[252, 253], [310, 311], [312, 313], [312, 314]]}, "_display_summary_report": {"executed_lines": [328, 329, 330, 335, 336, 337, 340, 341, 343, 344, 346, 347, 357, 358, 359, 360, 362, 363, 364, 366, 367, 368, 369, 370, 371, 376, 377, 378, 379, 381, 382, 383, 385, 386, 387, 388, 395, 396, 397, 398, 399, 401], "summary": {"covered_lines": 42, "num_statements": 46, "percent_covered": 88.88888888888889, "percent_covered_display": "88.89", "missing_lines": 4, "excluded_lines": 0, "num_branches": 26, "num_partial_branches": 2, "covered_branches": 22, "missing_branches": 4}, "missing_lines": [373, 389, 390, 392], "excluded_lines": [], "executed_branches": [[329, 330], [329, 335], [359, 360], [359, 362], [362, 363], [362, 376], [366, 367], [366, 370], [368, 362], [368, 369], [370, 371], [378, 379], [378, 381], [381, 382], [381, 395], [385, 386], [387, 381], [387, 388], [396, 397], [396, 401], [398, 399], [398, 401]], "missing_branches": [[370, 373], [385, 389], [389, 390], [389, 392]]}, "_display_query_validation_results": {"executed_lines": [410, 411, 412, 417, 418, 419, 422, 423, 425, 426, 428, 430, 431, 432, 435, 437, 438, 445, 446, 447, 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 0, "covered_branches": 24, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[411, 412], [411, 417], [417, 418], [417, 422], [425, 426], [425, 445], [426, 428], [426, 430], [431, 432], [431, 435], [445, 446], [445, 454], [447, 448], [447, 454], [449, 450], [449, 451], [451, 447], [451, 452], [454, -404], [454, 455], [456, -404], [456, 457], [458, 456], [458, 459]], "missing_branches": []}, "_has_critical_errors": {"executed_lines": [468, 469, 470, 476, 477, 478, 479, 480, 481, 485, 488, 489, 490, 491, 492, 493, 497, 499, 501, 502], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 95.23809523809524, "percent_covered_display": "95.24", "missing_lines": 0, "excluded_lines": 0, "num_branches": 22, "num_partial_branches": 2, "covered_branches": 20, "missing_branches": 2}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[469, 470], [469, 476], [477, 478], [477, 488], [478, 479], [478, 488], [479, 478], [479, 480], [480, 478], [480, 481], [481, 480], [481, 485], [489, 490], [490, 491], [490, 499], [491, 492], [492, 490], [492, 493], [493, 492], [493, 497]], "missing_branches": [[489, 499], [491, 490]]}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 10, 17, 23, 24, 25, 31, 32, 37, 42, 48, 141, 142, 147, 152, 227, 228, 229, 234, 240, 322, 404, 462], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 6, 8, 10, 17, 23, 24, 25, 27, 28, 31, 32, 37, 42, 48, 50, 52, 61, 62, 63, 64, 69, 70, 75, 76, 77, 78, 79, 83, 84, 91, 92, 93, 94, 95, 100, 101, 102, 104, 105, 106, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 124, 125, 126, 128, 129, 133, 134, 135, 136, 137, 138, 141, 142, 147, 152, 154, 156, 165, 166, 167, 168, 173, 174, 179, 180, 181, 182, 183, 187, 188, 195, 196, 197, 198, 199, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 214, 215, 219, 220, 221, 222, 223, 224, 227, 228, 229, 234, 240, 242, 244, 245, 252, 261, 262, 263, 264, 269, 270, 275, 276, 277, 278, 279, 285, 286, 287, 289, 290, 291, 296, 297, 298, 299, 304, 307, 309, 310, 314, 315, 316, 317, 322, 328, 329, 330, 335, 336, 337, 340, 341, 343, 344, 346, 347, 357, 358, 359, 360, 362, 363, 364, 366, 367, 368, 369, 370, 371, 376, 377, 378, 379, 381, 382, 383, 385, 386, 387, 388, 395, 396, 397, 398, 399, 401, 404, 410, 411, 412, 417, 418, 419, 422, 423, 425, 426, 428, 430, 431, 432, 435, 437, 438, 445, 446, 447, 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459, 462, 468, 469, 470, 476, 477, 478, 479, 480, 481, 485, 488, 489, 490, 491, 492, 493, 497, 499, 501, 502], "summary": {"covered_lines": 240, "num_statements": 261, "percent_covered": 89.21832884097034, "percent_covered_display": "89.22", "missing_lines": 21, "excluded_lines": 0, "num_branches": 110, "num_partial_branches": 11, "covered_branches": 91, "missing_branches": 19}, "missing_lines": [53, 130, 131, 132, 157, 216, 217, 218, 253, 300, 305, 306, 311, 312, 313, 318, 319, 373, 389, 390, 392], "excluded_lines": [], "executed_branches": [[27, -23], [27, 28], [52, 61], [101, 102], [101, 104], [121, 122], [121, 125], [123, 124], [123, 125], [129, 133], [133, 134], [133, 135], [156, 165], [203, 204], [215, 219], [219, 220], [219, 221], [244, 245], [244, 252], [252, 261], [286, 287], [286, 289], [310, 314], [314, 315], [314, 316], [329, 330], [329, 335], [359, 360], [359, 362], [362, 363], [362, 376], [366, 367], [366, 370], [368, 362], [368, 369], [370, 371], [378, 379], [378, 381], [381, 382], [381, 395], [385, 386], [387, 381], [387, 388], [396, 397], [396, 401], [398, 399], [398, 401], [411, 412], [411, 417], [417, 418], [417, 422], [425, 426], [425, 445], [426, 428], [426, 430], [431, 432], [431, 435], [445, 446], [445, 454], [447, 448], [447, 454], [449, 450], [449, 451], [451, 447], [451, 452], [454, -404], [454, 455], [456, -404], [456, 457], [458, 456], [458, 459], [469, 470], [469, 476], [477, 478], [477, 488], [478, 479], [478, 488], [479, 478], [479, 480], [480, 478], [480, 481], [481, 480], [481, 485], [489, 490], [490, 491], [490, 499], [491, 492], [492, 490], [492, 493], [493, 492], [493, 497]], "missing_branches": [[52, 53], [129, 130], [131, 132], [131, 133], [156, 157], [203, -141], [215, 216], [217, 218], [217, 219], [252, 253], [310, 311], [312, 313], [312, 314], [370, 373], [385, 389], [389, 390], [389, 392], [489, 499], [491, 490]]}}}, "src/toady/error_handling.py": {"executed_lines": [1, 8, 9, 10, 12, 30, 31, 34, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 54, 55, 56, 59, 60, 61, 62, 63, 64, 65, 68, 69, 70, 73, 74, 75, 78, 79, 82, 98, 113, 128, 145, 161, 177, 194, 211, 228, 246, 247, 249, 250, 260, 268, 269, 270, 273, 274, 275, 276, 277, 279, 280, 281, 282, 283, 285, 286, 287, 288, 289, 290, 291, 293, 294, 295, 296, 297, 299, 300, 301, 302, 303, 305, 307, 308, 309, 310, 311, 312, 315, 319, 320, 330, 346, 347, 348, 351, 352, 357, 360, 363, 371, 372, 375, 376, 378, 379, 382, 383, 386, 401, 403, 404, 405, 406, 408, 409, 410, 411, 413], "summary": {"covered_lines": 116, "num_statements": 116, "percent_covered": 96.875, "percent_covered_display": "96.88", "missing_lines": 0, "excluded_lines": 0, "num_branches": 44, "num_partial_branches": 5, "covered_branches": 39, "missing_branches": 5}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[269, 270], [269, 273], [273, 274], [273, 279], [275, 276], [279, 280], [279, 285], [281, 282], [285, 286], [285, 293], [287, 288], [287, 289], [289, 290], [289, 291], [293, 294], [293, 299], [295, 296], [299, 300], [299, 305], [301, 302], [305, 307], [305, 315], [308, 309], [310, 311], [310, 312], [347, 348], [347, 351], [351, 352], [351, 360], [375, 376], [375, 382], [403, 404], [403, 408], [405, 406], [405, 408], [408, 409], [408, 413], [410, 411], [410, 413]], "missing_branches": [[275, 277], [281, 283], [295, 297], [301, 303], [308, 312]], "functions": {"ErrorMessageFormatter.format_error": {"executed_lines": [260, 268, 269, 270, 273, 274, 275, 276, 277, 279, 280, 281, 282, 283, 285, 286, 287, 288, 289, 290, 291, 293, 294, 295, 296, 297, 299, 300, 301, 302, 303, 305, 307, 308, 309, 310, 311, 312, 315], "summary": {"covered_lines": 39, "num_statements": 39, "percent_covered": 92.7536231884058, "percent_covered_display": "92.75", "missing_lines": 0, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 5, "covered_branches": 25, "missing_branches": 5}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[269, 270], [269, 273], [273, 274], [273, 279], [275, 276], [279, 280], [279, 285], [281, 282], [285, 286], [285, 293], [287, 288], [287, 289], [289, 290], [289, 291], [293, 294], [293, 299], [295, 296], [299, 300], [299, 305], [301, 302], [305, 307], [305, 315], [308, 309], [310, 311], [310, 312]], "missing_branches": [[275, 277], [281, 283], [295, 297], [301, 303], [308, 312]]}, "ErrorMessageFormatter.get_exit_code": {"executed_lines": [330, 346, 347, 348, 351, 352, 357, 360], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[347, 348], [347, 351], [351, 352], [351, 360]], "missing_branches": []}, "handle_error": {"executed_lines": [371, 372, 375, 376, 378, 379, 382, 383], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[375, 376], [375, 382]], "missing_branches": []}, "create_user_friendly_error": {"executed_lines": [401, 403, 404, 405, 406, 408, 409, 410, 411, 413], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[403, 404], [403, 408], [405, 406], [405, 408], [408, 409], [408, 413], [410, 411], [410, 413]], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 10, 12, 30, 31, 34, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 54, 55, 56, 59, 60, 61, 62, 63, 64, 65, 68, 69, 70, 73, 74, 75, 78, 79, 82, 98, 113, 128, 145, 161, 177, 194, 211, 228, 246, 247, 249, 250, 319, 320, 363, 386], "summary": {"covered_lines": 51, "num_statements": 51, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ExitCode": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ErrorMessageTemplates": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ErrorMessageFormatter": {"executed_lines": [260, 268, 269, 270, 273, 274, 275, 276, 277, 279, 280, 281, 282, 283, 285, 286, 287, 288, 289, 290, 291, 293, 294, 295, 296, 297, 299, 300, 301, 302, 303, 305, 307, 308, 309, 310, 311, 312, 315, 330, 346, 347, 348, 351, 352, 357, 360], "summary": {"covered_lines": 47, "num_statements": 47, "percent_covered": 93.82716049382717, "percent_covered_display": "93.83", "missing_lines": 0, "excluded_lines": 0, "num_branches": 34, "num_partial_branches": 5, "covered_branches": 29, "missing_branches": 5}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[269, 270], [269, 273], [273, 274], [273, 279], [275, 276], [279, 280], [279, 285], [281, 282], [285, 286], [285, 293], [287, 288], [287, 289], [289, 290], [289, 291], [293, 294], [293, 299], [295, 296], [299, 300], [299, 305], [301, 302], [305, 307], [305, 315], [308, 309], [310, 311], [310, 312], [347, 348], [347, 351], [351, 352], [351, 360]], "missing_branches": [[275, 277], [281, 283], [295, 297], [301, 303], [308, 312]]}, "": {"executed_lines": [1, 8, 9, 10, 12, 30, 31, 34, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 54, 55, 56, 59, 60, 61, 62, 63, 64, 65, 68, 69, 70, 73, 74, 75, 78, 79, 82, 98, 113, 128, 145, 161, 177, 194, 211, 228, 246, 247, 249, 250, 319, 320, 363, 371, 372, 375, 376, 378, 379, 382, 383, 386, 401, 403, 404, 405, 406, 408, 409, 410, 411, 413], "summary": {"covered_lines": 69, "num_statements": 69, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[375, 376], [375, 382], [403, 404], [403, 408], [405, 406], [405, 408], [408, 409], [408, 413], [410, 411], [410, 413]], "missing_branches": []}}}, "src/toady/exceptions.py": {"executed_lines": [1, 7, 8, 11, 12, 14, 15, 16, 17, 20, 21, 24, 25, 26, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 58, 59, 60, 61, 64, 65, 66, 67, 68, 71, 72, 79, 96, 97, 98, 99, 100, 101, 103, 105, 107, 113, 123, 124, 131, 148, 154, 155, 156, 159, 160, 161, 162, 163, 164, 167, 168, 174, 184, 190, 191, 192, 195, 196, 201, 216, 222, 223, 224, 225, 226, 227, 230, 231, 237, 252, 258, 259, 260, 261, 262, 263, 266, 267, 273, 280, 288, 289, 291, 300, 313, 314, 316, 325, 338, 339, 341, 356, 362, 363, 364, 365, 366, 367, 370, 371, 373, 386, 397, 398, 399, 402, 403, 405, 418, 429, 430, 431, 434, 435, 437, 452, 458, 459, 460, 461, 462, 463, 466, 467, 469, 484, 495, 496, 497, 498, 499, 500, 503, 504, 506, 523, 529, 530, 531, 532, 533, 534, 535, 536, 537, 541, 542, 544, 551, 559, 560, 562, 569, 576, 577, 579, 589, 599, 600, 601, 604, 605, 607, 614, 621, 622, 624, 634, 644, 645, 646, 649, 650, 652, 662, 672, 673, 674, 679, 682, 699, 700, 704, 712, 727], "summary": {"covered_lines": 183, "num_statements": 183, "percent_covered": 99.56331877729258, "percent_covered_display": "99.56", "missing_lines": 0, "excluded_lines": 0, "num_branches": 46, "num_partial_branches": 1, "covered_branches": 45, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[159, 160], [159, 161], [161, 162], [161, 163], [163, -131], [163, 164], [191, -174], [191, 192], [224, 225], [224, 226], [226, -201], [226, 227], [260, 261], [260, 262], [262, -237], [262, 263], [364, 365], [364, 366], [366, -341], [366, 367], [398, -373], [398, 399], [430, -405], [430, 431], [460, 461], [460, 462], [462, -437], [462, 463], [497, 498], [497, 499], [499, -469], [499, 500], [532, 533], [532, 534], [534, 535], [534, 536], [536, -506], [536, 537], [600, 601], [645, -624], [645, 646], [673, -652], [673, 674], [699, 700], [699, 704]], "missing_branches": [[600, -579]], "functions": {"ToadyError.__init__": {"executed_lines": [96, 97, 98, 99, 100, 101], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ToadyError.__str__": {"executed_lines": [105], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ToadyError.to_dict": {"executed_lines": [113], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ValidationError.__init__": {"executed_lines": [148, 154, 155, 156, 159, 160, 161, 162, 163, 164], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[159, 160], [159, 161], [161, 162], [161, 163], [163, -131], [163, 164]], "missing_branches": []}, "ConfigurationError.__init__": {"executed_lines": [184, 190, 191, 192], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[191, -174], [191, 192]], "missing_branches": []}, "FileOperationError.__init__": {"executed_lines": [216, 222, 223, 224, 225, 226, 227], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[224, 225], [224, 226], [226, -201], [226, 227]], "missing_branches": []}, "NetworkError.__init__": {"executed_lines": [252, 258, 259, 260, 261, 262, 263], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[260, 261], [260, 262], [262, -237], [262, 263]], "missing_branches": []}, "GitHubServiceError.__init__": {"executed_lines": [280], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubCLINotFoundError.__init__": {"executed_lines": [300], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAuthenticationError.__init__": {"executed_lines": [325], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAPIError.__init__": {"executed_lines": [356, 362, 363, 364, 365, 366, 367], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[364, 365], [364, 366], [366, -341], [366, 367]], "missing_branches": []}, "GitHubTimeoutError.__init__": {"executed_lines": [386, 397, 398, 399], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[398, -373], [398, 399]], "missing_branches": []}, "GitHubRateLimitError.__init__": {"executed_lines": [418, 429, 430, 431], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[430, -405], [430, 431]], "missing_branches": []}, "GitHubNotFoundError.__init__": {"executed_lines": [452, 458, 459, 460, 461, 462, 463], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[460, 461], [460, 462], [462, -437], [462, 463]], "missing_branches": []}, "GitHubPermissionError.__init__": {"executed_lines": [484, 495, 496, 497, 498, 499, 500], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[497, 498], [497, 499], [499, -469], [499, 500]], "missing_branches": []}, "CommandExecutionError.__init__": {"executed_lines": [523, 529, 530, 531, 532, 533, 534, 535, 536, 537], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[532, 533], [532, 534], [534, 535], [534, 536], [536, -506], [536, 537]], "missing_branches": []}, "FetchServiceError.__init__": {"executed_lines": [551], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyServiceError.__init__": {"executed_lines": [569], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "CommentNotFoundError.__init__": {"executed_lines": [589, 599, 600, 601], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[600, 601]], "missing_branches": [[600, -579]]}, "ResolveServiceError.__init__": {"executed_lines": [614], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ThreadNotFoundError.__init__": {"executed_lines": [634, 644, 645, 646], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[645, -624], [645, 646]], "missing_branches": []}, "ThreadPermissionError.__init__": {"executed_lines": [662, 672, 673, 674], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[673, -652], [673, 674]], "missing_branches": []}, "create_validation_error": {"executed_lines": [699, 700, 704], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[699, 700], [699, 704]], "missing_branches": []}, "create_github_error": {"executed_lines": [727], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 11, 12, 14, 15, 16, 17, 20, 21, 24, 25, 26, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 58, 59, 60, 61, 64, 65, 66, 67, 68, 71, 72, 79, 103, 107, 123, 124, 131, 167, 168, 174, 195, 196, 201, 230, 231, 237, 266, 267, 273, 288, 289, 291, 313, 314, 316, 338, 339, 341, 370, 371, 373, 402, 403, 405, 434, 435, 437, 466, 467, 469, 503, 504, 506, 541, 542, 544, 559, 560, 562, 576, 577, 579, 604, 605, 607, 621, 622, 624, 649, 650, 652, 679, 682, 712], "summary": {"covered_lines": 86, "num_statements": 86, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ErrorSeverity": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ErrorCode": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ToadyError": {"executed_lines": [96, 97, 98, 99, 100, 101, 105, 113], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ValidationError": {"executed_lines": [148, 154, 155, 156, 159, 160, 161, 162, 163, 164], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[159, 160], [159, 161], [161, 162], [161, 163], [163, -131], [163, 164]], "missing_branches": []}, "ConfigurationError": {"executed_lines": [184, 190, 191, 192], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[191, -174], [191, 192]], "missing_branches": []}, "FileOperationError": {"executed_lines": [216, 222, 223, 224, 225, 226, 227], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[224, 225], [224, 226], [226, -201], [226, 227]], "missing_branches": []}, "NetworkError": {"executed_lines": [252, 258, 259, 260, 261, 262, 263], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[260, 261], [260, 262], [262, -237], [262, 263]], "missing_branches": []}, "GitHubServiceError": {"executed_lines": [280], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubCLINotFoundError": {"executed_lines": [300], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAuthenticationError": {"executed_lines": [325], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAPIError": {"executed_lines": [356, 362, 363, 364, 365, 366, 367], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[364, 365], [364, 366], [366, -341], [366, 367]], "missing_branches": []}, "GitHubTimeoutError": {"executed_lines": [386, 397, 398, 399], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[398, -373], [398, 399]], "missing_branches": []}, "GitHubRateLimitError": {"executed_lines": [418, 429, 430, 431], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[430, -405], [430, 431]], "missing_branches": []}, "GitHubNotFoundError": {"executed_lines": [452, 458, 459, 460, 461, 462, 463], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[460, 461], [460, 462], [462, -437], [462, 463]], "missing_branches": []}, "GitHubPermissionError": {"executed_lines": [484, 495, 496, 497, 498, 499, 500], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[497, 498], [497, 499], [499, -469], [499, 500]], "missing_branches": []}, "CommandExecutionError": {"executed_lines": [523, 529, 530, 531, 532, 533, 534, 535, 536, 537], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[532, 533], [532, 534], [534, 535], [534, 536], [536, -506], [536, 537]], "missing_branches": []}, "FetchServiceError": {"executed_lines": [551], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyServiceError": {"executed_lines": [569], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "CommentNotFoundError": {"executed_lines": [589, 599, 600, 601], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[600, 601]], "missing_branches": [[600, -579]]}, "ResolveServiceError": {"executed_lines": [614], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ThreadNotFoundError": {"executed_lines": [634, 644, 645, 646], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[645, -624], [645, 646]], "missing_branches": []}, "ThreadPermissionError": {"executed_lines": [662, 672, 673, 674], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[673, -652], [673, 674]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 11, 12, 14, 15, 16, 17, 20, 21, 24, 25, 26, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 58, 59, 60, 61, 64, 65, 66, 67, 68, 71, 72, 79, 103, 107, 123, 124, 131, 167, 168, 174, 195, 196, 201, 230, 231, 237, 266, 267, 273, 288, 289, 291, 313, 314, 316, 338, 339, 341, 370, 371, 373, 402, 403, 405, 434, 435, 437, 466, 467, 469, 503, 504, 506, 541, 542, 544, 559, 560, 562, 576, 577, 579, 604, 605, 607, 621, 622, 624, 649, 650, 652, 679, 682, 699, 700, 704, 712, 727], "summary": {"covered_lines": 90, "num_statements": 90, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[699, 700], [699, 704]], "missing_branches": []}}}, "src/toady/formatters/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/formatters/format_interfaces.py": {"executed_lines": [1, 8, 9, 11, 14, 15, 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 111, 130, 131, 137, 143, 145, 154, 155, 156, 157, 158, 159, 160, 161, 164, 165, 167, 168, 169, 170, 172, 184, 185, 186, 188, 190, 200, 202, 206, 207, 208, 209, 211, 215, 216, 217, 218, 221, 222, 224, 233, 234, 237, 238, 240, 257, 258, 259, 260, 261, 263, 269, 278, 279, 281, 283, 284, 291, 293, 294, 307, 308, 309, 313, 314, 315, 316, 317, 318, 322, 323, 329, 331, 332, 341], "summary": {"covered_lines": 76, "num_statements": 83, "percent_covered": 88.57142857142857, "percent_covered_display": "88.57", "missing_lines": 7, "excluded_lines": 66, "num_branches": 22, "num_partial_branches": 1, "covered_branches": 17, "missing_branches": 5}, "missing_lines": [105, 106, 109, 123, 124, 127, 187], "excluded_lines": [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91], "executed_branches": [[154, 155], [154, 156], [156, 157], [156, 158], [158, 159], [158, 160], [160, 161], [160, 164], [184, 185], [184, 186], [186, 188], [207, 208], [207, 209], [216, 217], [216, 218], [307, 308], [307, 313]], "missing_branches": [[105, 106], [105, 109], [123, 124], [123, 127], [186, 187]], "functions": {"IFormatter.format_threads": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [23, 24, 25, 26, 27, 28, 29, 30, 31], "executed_branches": [], "missing_branches": []}, "IFormatter.format_comments": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [35, 36, 37, 38, 39, 40, 41, 42, 43], "executed_branches": [], "missing_branches": []}, "IFormatter.format_object": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [47, 48, 49, 50, 51, 52, 53, 54, 55], "executed_branches": [], "missing_branches": []}, "IFormatter.format_array": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [59, 60, 61, 62, 63, 64, 65, 66, 67], "executed_branches": [], "missing_branches": []}, "IFormatter.format_primitive": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [71, 72, 73, 74, 75, 76, 77, 78, 79], "executed_branches": [], "missing_branches": []}, "IFormatter.format_error": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [83, 84, 85, 86, 87, 88, 89, 90, 91], "executed_branches": [], "missing_branches": []}, "IFormatter.format_success_message": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [105, 106, 109], "excluded_lines": [], "executed_branches": [], "missing_branches": [[105, 106], [105, 109]]}, "IFormatter.format_warning_message": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [123, 124, 127], "excluded_lines": [], "executed_branches": [], "missing_branches": [[123, 124], [123, 127]]}, "BaseFormatter.__init__": {"executed_lines": [143], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "BaseFormatter._safe_serialize": {"executed_lines": [154, 155, 156, 157, 158, 159, 160, 161, 164, 165, 167, 168, 169, 170], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[154, 155], [154, 156], [156, 157], [156, 158], [158, 159], [158, 160], [160, 161], [160, 164]], "missing_branches": []}, "BaseFormatter._handle_empty_data": {"executed_lines": [184, 185, 186, 188], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 77.77777777777777, "percent_covered_display": "77.78", "missing_lines": 1, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [187], "excluded_lines": [], "executed_branches": [[184, 185], [184, 186], [186, 188]], "missing_branches": [[186, 187]]}, "BaseFormatter.format_comments": {"executed_lines": [200], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "BaseFormatter.format_success_message": {"executed_lines": [206, 207, 208, 209], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[207, 208], [207, 209]], "missing_branches": []}, "BaseFormatter.format_warning_message": {"executed_lines": [215, 216, 217, 218], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[216, 217], [216, 218]], "missing_branches": []}, "FormatterError.__init__": {"executed_lines": [233, 234], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterOptions.__init__": {"executed_lines": [257, 258, 259, 260, 261], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterOptions.to_dict": {"executed_lines": [269], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory.register": {"executed_lines": [291], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory.create": {"executed_lines": [307, 308, 309, 313, 314, 315, 316, 317, 318], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[307, 308], [307, 313]], "missing_branches": []}, "FormatterFactory.list_formatters": {"executed_lines": [329], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory.is_registered": {"executed_lines": [341], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 11, 14, 15, 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 111, 130, 131, 137, 145, 172, 190, 202, 211, 221, 222, 224, 237, 238, 240, 263, 278, 279, 281, 283, 284, 293, 294, 322, 323, 331, 332], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 12, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82], "executed_branches": [], "missing_branches": []}}, "classes": {"IFormatter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 6, "excluded_lines": 54, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4}, "missing_lines": [105, 106, 109, 123, 124, 127], "excluded_lines": [23, 24, 25, 26, 27, 28, 29, 30, 31, 35, 36, 37, 38, 39, 40, 41, 42, 43, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 61, 62, 63, 64, 65, 66, 67, 71, 72, 73, 74, 75, 76, 77, 78, 79, 83, 84, 85, 86, 87, 88, 89, 90, 91], "executed_branches": [], "missing_branches": [[105, 106], [105, 109], [123, 124], [123, 127]]}, "BaseFormatter": {"executed_lines": [143, 154, 155, 156, 157, 158, 159, 160, 161, 164, 165, 167, 168, 169, 170, 184, 185, 186, 188, 200, 206, 207, 208, 209, 215, 216, 217, 218], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 95.55555555555556, "percent_covered_display": "95.56", "missing_lines": 1, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 1, "covered_branches": 15, "missing_branches": 1}, "missing_lines": [187], "excluded_lines": [], "executed_branches": [[154, 155], [154, 156], [156, 157], [156, 158], [158, 159], [158, 160], [160, 161], [160, 164], [184, 185], [184, 186], [186, 188], [207, 208], [207, 209], [216, 217], [216, 218]], "missing_branches": [[186, 187]]}, "FormatterError": {"executed_lines": [233, 234], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterOptions": {"executed_lines": [257, 258, 259, 260, 261, 269], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory": {"executed_lines": [291, 307, 308, 309, 313, 314, 315, 316, 317, 318, 329, 341], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[307, 308], [307, 313]], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 11, 14, 15, 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 111, 130, 131, 137, 145, 172, 190, 202, 211, 221, 222, 224, 237, 238, 240, 263, 278, 279, 281, 283, 284, 293, 294, 322, 323, 331, 332], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 12, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82], "executed_branches": [], "missing_branches": []}}}, "src/toady/formatters/format_selection.py": {"executed_lines": [1, 7, 8, 10, 14, 15, 16, 20, 26, 29, 30, 31, 33, 34, 36, 38, 39, 40, 44, 45, 49, 50, 53, 54, 57, 58, 61, 62, 65, 66, 74, 75, 83, 86, 87, 88, 90, 91, 92, 96, 99, 102, 103, 105, 112, 113, 116, 123, 125, 126, 129, 132, 145, 147, 149, 150, 156, 159, 173, 174, 177, 178, 181, 184, 198, 200, 201, 202, 203, 208, 217, 218, 220, 228, 230, 233, 242, 249, 251, 257, 265, 267, 268, 270, 273, 274, 275, 278, 285, 286, 288, 289, 291, 292, 293, 296, 297, 298, 301, 311, 312, 314, 315, 316, 317, 320, 321, 322, 325, 332, 333, 335, 338, 339, 340], "summary": {"covered_lines": 113, "num_statements": 129, "percent_covered": 87.42138364779875, "percent_covered_display": "87.42", "missing_lines": 16, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 0, "covered_branches": 26, "missing_branches": 4}, "missing_lines": [41, 42, 46, 47, 51, 55, 59, 63, 69, 70, 71, 72, 78, 79, 80, 81], "excluded_lines": [], "executed_branches": [[29, 30], [29, 86], [86, -20], [86, 87], [125, 126], [125, 129], [149, 150], [149, 156], [173, 174], [173, 177], [177, 178], [177, 181], [265, 267], [265, 268], [268, 270], [268, 273], [285, 286], [285, 289], [289, 291], [289, 296], [311, 312], [311, 320], [315, 316], [315, 317], [332, 333], [332, 338]], "missing_branches": [[70, 71], [70, 72], [79, 80], [79, 81]], "functions": {"_ensure_formatters_registered": {"executed_lines": [26, 29, 30, 31, 33, 34, 36, 38, 39, 40, 44, 45, 49, 50, 53, 54, 57, 58, 61, 62, 65, 66, 74, 75, 83, 86, 87, 88, 90, 91, 92], "summary": {"covered_lines": 31, "num_statements": 47, "percent_covered": 63.63636363636363, "percent_covered_display": "63.64", "missing_lines": 16, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 4}, "missing_lines": [41, 42, 46, 47, 51, 55, 59, 63, 69, 70, 71, 72, 78, 79, 80, 81], "excluded_lines": [], "executed_branches": [[29, 30], [29, 86], [86, -20], [86, 87]], "missing_branches": [[70, 71], [70, 72], [79, 80], [79, 81]]}, "FormatSelectionError.__init__": {"executed_lines": [112, 113], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "get_default_format": {"executed_lines": [123, 125, 126, 129], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[125, 126], [125, 129]], "missing_branches": []}, "validate_format": {"executed_lines": [145, 147, 149, 150, 156], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[149, 150], [149, 156]], "missing_branches": []}, "resolve_format_from_options": {"executed_lines": [173, 174, 177, 178, 181], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[173, 174], [173, 177], [177, 178], [177, 181]], "missing_branches": []}, "create_formatter": {"executed_lines": [198, 200, 201, 202, 203], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_format_option": {"executed_lines": [217, 218, 220, 228, 230], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_legacy_pretty_option": {"executed_lines": [242, 249, 251], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_threads_output": {"executed_lines": [265, 267, 268, 270, 273, 274, 275], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[265, 267], [265, 268], [268, 270], [268, 273]], "missing_branches": []}, "format_object_output": {"executed_lines": [285, 286, 288, 289, 291, 292, 293, 296, 297, 298], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[285, 286], [285, 289], [289, 291], [289, 296]], "missing_branches": []}, "format_success_message": {"executed_lines": [311, 312, 314, 315, 316, 317, 320, 321, 322], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[311, 312], [311, 320], [315, 316], [315, 317]], "missing_branches": []}, "format_error_message": {"executed_lines": [332, 333, 335, 338, 339, 340], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[332, 333], [332, 338]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 10, 14, 15, 16, 20, 96, 99, 102, 103, 105, 116, 132, 159, 184, 208, 233, 257, 278, 301, 325], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"FormatSelectionError": {"executed_lines": [112, 113], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 10, 14, 15, 16, 20, 26, 29, 30, 31, 33, 34, 36, 38, 39, 40, 44, 45, 49, 50, 53, 54, 57, 58, 61, 62, 65, 66, 74, 75, 83, 86, 87, 88, 90, 91, 92, 96, 99, 102, 103, 105, 116, 123, 125, 126, 129, 132, 145, 147, 149, 150, 156, 159, 173, 174, 177, 178, 181, 184, 198, 200, 201, 202, 203, 208, 217, 218, 220, 228, 230, 233, 242, 249, 251, 257, 265, 267, 268, 270, 273, 274, 275, 278, 285, 286, 288, 289, 291, 292, 293, 296, 297, 298, 301, 311, 312, 314, 315, 316, 317, 320, 321, 322, 325, 332, 333, 335, 338, 339, 340], "summary": {"covered_lines": 111, "num_statements": 127, "percent_covered": 87.26114649681529, "percent_covered_display": "87.26", "missing_lines": 16, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 0, "covered_branches": 26, "missing_branches": 4}, "missing_lines": [41, 42, 46, 47, 51, 55, 59, 63, 69, 70, 71, 72, 78, 79, 80, 81], "excluded_lines": [], "executed_branches": [[29, 30], [29, 86], [86, -20], [86, 87], [125, 126], [125, 129], [149, 150], [149, 156], [173, 174], [173, 177], [177, 178], [177, 181], [265, 267], [265, 268], [268, 270], [268, 273], [285, 286], [285, 289], [289, 291], [289, 296], [311, 312], [311, 320], [315, 316], [315, 317], [332, 333], [332, 338]], "missing_branches": [[70, 71], [70, 72], [79, 80], [79, 81]]}}}, "src/toady/formatters/formatters.py": {"executed_lines": [1, 7, 8, 9, 11, 13, 14, 15, 18, 19, 21, 22, 32, 33, 35, 38, 39, 41, 42, 51, 52, 55, 56, 58, 59, 70, 71, 73, 81, 82, 84, 85, 86, 90, 92, 93, 102, 103, 105, 108, 111, 112, 113, 115, 118, 119, 120, 123, 124, 126, 128, 129, 142, 145, 146, 149, 151, 152, 154, 156, 159, 161, 162, 164, 165, 168, 173, 174, 175, 176, 177, 182, 185, 187, 188, 197, 198, 200, 203, 204, 206, 208, 209, 210, 211, 215, 216, 219, 220, 223, 224, 228, 229, 230, 233, 236, 239, 240, 243, 244, 245, 248, 250, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 267, 271, 272, 273, 274, 275, 276, 277, 280, 282, 283, 294, 296, 297, 307, 310, 328, 330, 331, 334, 337, 338, 341, 342, 345, 348, 349, 353, 356, 357, 359], "summary": {"covered_lines": 140, "num_statements": 153, "percent_covered": 89.14027149321267, "percent_covered_display": "89.14", "missing_lines": 13, "excluded_lines": 0, "num_branches": 68, "num_partial_branches": 11, "covered_branches": 57, "missing_branches": 11}, "missing_lines": [88, 147, 166, 167, 170, 179, 183, 212, 213, 225, 278, 360, 362], "excluded_lines": [], "executed_branches": [[32, 33], [32, 35], [70, 71], [70, 73], [84, 85], [84, 90], [85, 86], [102, 103], [102, 105], [111, 112], [111, 118], [112, 113], [112, 115], [118, 119], [118, 123], [123, 124], [123, 126], [146, 149], [151, 152], [151, 154], [159, 161], [164, 165], [164, 182], [165, 168], [168, 173], [173, 174], [175, 164], [175, 176], [176, 177], [182, 185], [197, 198], [197, 200], [206, 208], [206, 265], [208, 209], [208, 211], [211, 215], [224, 228], [243, 244], [243, 260], [250, 251], [250, 260], [256, 250], [256, 257], [260, 206], [260, 261], [273, 274], [273, 275], [275, 276], [275, 277], [277, 280], [328, 330], [328, 348], [330, 331], [330, 337], [341, -310], [341, 342]], "missing_branches": [[85, 88], [146, 147], [159, 182], [165, 166], [168, 170], [173, 179], [176, 175], [182, 183], [211, 212], [224, 225], [277, 278]], "functions": {"OutputFormatter.format_threads": {"executed_lines": [32, 33, 35], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[32, 33], [32, 35]], "missing_branches": []}, "JSONFormatter.format_threads": {"executed_lines": [51, 52], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._wrap_text": {"executed_lines": [70, 71, 73, 81, 82, 84, 85, 86, 90], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 87.5, "percent_covered_display": "87.50", "missing_lines": 1, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [88], "excluded_lines": [], "executed_branches": [[70, 71], [70, 73], [84, 85], [84, 90], [85, 86]], "missing_branches": [[85, 88]]}, "PrettyFormatter._format_file_context": {"executed_lines": [102, 103, 105, 108, 111, 112, 113, 115, 118, 119, 120, 123, 124, 126], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[102, 103], [102, 105], [111, 112], [111, 118], [112, 113], [112, 115], [118, 119], [118, 123], [123, 124], [123, 126]], "missing_branches": []}, "PrettyFormatter._format_comment": {"executed_lines": [142, 145, 146, 149, 151, 152, 154, 156, 159, 161, 162, 164, 165, 168, 173, 174, 175, 176, 177, 182, 185], "summary": {"covered_lines": 21, "num_statements": 27, "percent_covered": 72.34042553191489, "percent_covered_display": "72.34", "missing_lines": 6, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 7, "covered_branches": 13, "missing_branches": 7}, "missing_lines": [147, 166, 167, 170, 179, 183], "excluded_lines": [], "executed_branches": [[146, 149], [151, 152], [151, 154], [159, 161], [164, 165], [164, 182], [165, 168], [168, 173], [173, 174], [175, 164], [175, 176], [176, 177], [182, 185]], "missing_branches": [[146, 147], [159, 182], [165, 166], [168, 170], [173, 179], [176, 175], [182, 183]]}, "PrettyFormatter.format_threads": {"executed_lines": [197, 198, 200, 203, 204, 206, 208, 209, 210, 211, 215, 216, 219, 220, 223, 224, 228, 229, 230, 233, 236, 239, 240, 243, 244, 245, 248, 250, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 267, 271, 272, 273, 274, 275, 276, 277, 280], "summary": {"covered_lines": 47, "num_statements": 51, "percent_covered": 90.66666666666667, "percent_covered_display": "90.67", "missing_lines": 4, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 3, "covered_branches": 21, "missing_branches": 3}, "missing_lines": [212, 213, 225, 278], "excluded_lines": [], "executed_branches": [[197, 198], [197, 200], [206, 208], [206, 265], [208, 209], [208, 211], [211, 215], [224, 228], [243, 244], [243, 260], [250, 251], [250, 260], [256, 250], [256, 257], [260, 206], [260, 261], [273, 274], [273, 275], [275, 276], [275, 277], [277, 280]], "missing_branches": [[211, 212], [224, 225], [277, 278]]}, "PrettyFormatter.format_progress_message": {"executed_lines": [294], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter.format_result_summary": {"executed_lines": [307], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_fetch_output": {"executed_lines": [328, 330, 331, 334, 337, 338, 341, 342, 345, 348, 349], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[328, 330], [328, 348], [330, 331], [330, 337], [341, -310], [341, 342]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 11, 13, 14, 15, 18, 19, 21, 22, 38, 39, 41, 42, 55, 56, 58, 59, 92, 93, 128, 129, 187, 188, 282, 283, 296, 297, 310, 353, 356, 357, 359], "summary": {"covered_lines": 31, "num_statements": 33, "percent_covered": 93.93939393939394, "percent_covered_display": "93.94", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [360, 362], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"OutputFormatter": {"executed_lines": [32, 33, 35], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[32, 33], [32, 35]], "missing_branches": []}, "JSONFormatter": {"executed_lines": [51, 52], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter": {"executed_lines": [70, 71, 73, 81, 82, 84, 85, 86, 90, 102, 103, 105, 108, 111, 112, 113, 115, 118, 119, 120, 123, 124, 126, 142, 145, 146, 149, 151, 152, 154, 156, 159, 161, 162, 164, 165, 168, 173, 174, 175, 176, 177, 182, 185, 197, 198, 200, 203, 204, 206, 208, 209, 210, 211, 215, 216, 219, 220, 223, 224, 228, 229, 230, 233, 236, 239, 240, 243, 244, 245, 248, 250, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 267, 271, 272, 273, 274, 275, 276, 277, 280, 294, 307], "summary": {"covered_lines": 93, "num_statements": 104, "percent_covered": 86.58536585365853, "percent_covered_display": "86.59", "missing_lines": 11, "excluded_lines": 0, "num_branches": 60, "num_partial_branches": 11, "covered_branches": 49, "missing_branches": 11}, "missing_lines": [88, 147, 166, 167, 170, 179, 183, 212, 213, 225, 278], "excluded_lines": [], "executed_branches": [[70, 71], [70, 73], [84, 85], [84, 90], [85, 86], [102, 103], [102, 105], [111, 112], [111, 118], [112, 113], [112, 115], [118, 119], [118, 123], [123, 124], [123, 126], [146, 149], [151, 152], [151, 154], [159, 161], [164, 165], [164, 182], [165, 168], [168, 173], [173, 174], [175, 164], [175, 176], [176, 177], [182, 185], [197, 198], [197, 200], [206, 208], [206, 265], [208, 209], [208, 211], [211, 215], [224, 228], [243, 244], [243, 260], [250, 251], [250, 260], [256, 250], [256, 257], [260, 206], [260, 261], [273, 274], [273, 275], [275, 276], [275, 277], [277, 280]], "missing_branches": [[85, 88], [146, 147], [159, 182], [165, 166], [168, 170], [173, 179], [176, 175], [182, 183], [211, 212], [224, 225], [277, 278]]}, "": {"executed_lines": [1, 7, 8, 9, 11, 13, 14, 15, 18, 19, 21, 22, 38, 39, 41, 42, 55, 56, 58, 59, 92, 93, 128, 129, 187, 188, 282, 283, 296, 297, 310, 328, 330, 331, 334, 337, 338, 341, 342, 345, 348, 349, 353, 356, 357, 359], "summary": {"covered_lines": 42, "num_statements": 44, "percent_covered": 96.0, "percent_covered_display": "96.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [360, 362], "excluded_lines": [], "executed_branches": [[328, 330], [328, 348], [330, 331], [330, 337], [341, -310], [341, 342]], "missing_branches": []}}}, "src/toady/formatters/json_formatter.py": {"executed_lines": [1, 8, 9, 11, 12, 15, 16, 22, 31, 32, 35, 41, 42, 44, 56, 58, 59, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 77, 79, 80, 81, 82, 86, 98, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 119, 121, 122, 123, 128, 140, 141, 142, 143, 144, 148, 160, 162, 163, 166, 167, 168, 169, 170, 172, 173, 174, 175, 180, 182, 183, 184, 189, 201, 202, 208, 220, 222, 223, 224, 225, 226, 228, 234, 246, 248, 249, 251, 253, 265, 267, 268, 270, 272, 284, 293, 304, 305, 306, 309, 310, 312, 314, 323, 325, 335, 336, 339, 340, 343, 344, 345, 350, 351, 352, 353, 355, 358, 359, 362, 363, 366, 367, 370, 371, 372, 389, 392, 401, 404, 413, 416, 425], "summary": {"covered_lines": 132, "num_statements": 150, "percent_covered": 89.80582524271844, "percent_covered_display": "89.81", "missing_lines": 18, "excluded_lines": 0, "num_branches": 56, "num_partial_branches": 3, "covered_branches": 53, "missing_branches": 3}, "missing_lines": [124, 185, 203, 204, 229, 230, 346, 347, 373, 375, 376, 377, 378, 381, 382, 383, 384, 385], "excluded_lines": [], "executed_branches": [[41, -22], [41, 42], [58, 59], [58, 62], [63, 64], [63, 77], [65, 66], [65, 68], [80, 81], [80, 82], [100, 101], [100, 104], [105, 106], [105, 119], [107, 108], [107, 110], [122, 123], [162, 163], [162, 166], [167, 168], [167, 180], [169, 170], [169, 172], [183, 184], [223, 224], [223, 225], [225, 226], [225, 228], [248, 249], [248, 251], [267, 268], [267, 270], [304, 305], [304, 309], [305, 304], [305, 306], [309, 310], [309, 312], [335, 336], [335, 339], [339, 340], [339, 343], [343, 344], [343, 350], [350, 351], [350, 358], [358, 359], [358, 362], [362, 363], [362, 366], [366, 367], [366, 370], [370, 371]], "missing_branches": [[122, 124], [183, 185], [370, 381]], "functions": {"JSONFormatter.__init__": {"executed_lines": [31, 32, 35, 41, 42], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[41, -22], [41, 42]], "missing_branches": []}, "JSONFormatter.format_threads": {"executed_lines": [56, 58, 59, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 77, 79, 80, 81, 82], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[58, 59], [58, 62], [63, 64], [63, 77], [65, 66], [65, 68], [80, 81], [80, 82]], "missing_branches": []}, "JSONFormatter.format_comments": {"executed_lines": [98, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 119, 121, 122, 123], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 92.3076923076923, "percent_covered_display": "92.31", "missing_lines": 1, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [124], "excluded_lines": [], "executed_branches": [[100, 101], [100, 104], [105, 106], [105, 119], [107, 108], [107, 110], [122, 123]], "missing_branches": [[122, 124]]}, "JSONFormatter.format_object": {"executed_lines": [140, 141, 142, 143, 144], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "JSONFormatter.format_array": {"executed_lines": [160, 162, 163, 166, 167, 168, 169, 170, 172, 173, 174, 175, 180, 182, 183, 184], "summary": {"covered_lines": 16, "num_statements": 17, "percent_covered": 92.0, "percent_covered_display": "92.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [185], "excluded_lines": [], "executed_branches": [[162, 163], [162, 166], [167, 168], [167, 180], [169, 170], [169, 172], [183, 184]], "missing_branches": [[183, 185]]}, "JSONFormatter.format_primitive": {"executed_lines": [201, 202], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [203, 204], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "JSONFormatter.format_error": {"executed_lines": [220, 222, 223, 224, 225, 226, 228], "summary": {"covered_lines": 7, "num_statements": 9, "percent_covered": 84.61538461538461, "percent_covered_display": "84.62", "missing_lines": 2, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [229, 230], "excluded_lines": [], "executed_branches": [[223, 224], [223, 225], [225, 226], [225, 228]], "missing_branches": []}, "JSONFormatter.format_success_message": {"executed_lines": [246, 248, 249, 251], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[248, 249], [248, 251]], "missing_branches": []}, "JSONFormatter.format_warning_message": {"executed_lines": [265, 267, 268, 270], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[267, 268], [267, 270]], "missing_branches": []}, "JSONFormatter.format_reply_result": {"executed_lines": [284, 293, 304, 305, 306, 309, 310, 312], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[304, 305], [304, 309], [305, 304], [305, 306], [309, 310], [309, 312]], "missing_branches": []}, "JSONFormatter.format_resolve_result": {"executed_lines": [323], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "JSONFormatter._safe_serialize": {"executed_lines": [335, 336, 339, 340, 343, 344, 345, 350, 351, 352, 353, 355, 358, 359, 362, 363, 366, 367, 370, 371, 372], "summary": {"covered_lines": 21, "num_statements": 33, "percent_covered": 73.46938775510205, "percent_covered_display": "73.47", "missing_lines": 12, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 1, "covered_branches": 15, "missing_branches": 1}, "missing_lines": [346, 347, 373, 375, 376, 377, 378, 381, 382, 383, 384, 385], "excluded_lines": [], "executed_branches": [[335, 336], [335, 339], [339, 340], [339, 343], [343, 344], [343, 350], [350, 351], [350, 358], [358, 359], [358, 362], [362, 363], [362, 366], [366, 367], [366, 370], [370, 371]], "missing_branches": [[370, 381]]}, "format_threads_json": {"executed_lines": [401], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_comments_json": {"executed_lines": [413], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_object_json": {"executed_lines": [425], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 11, 12, 15, 16, 22, 44, 86, 128, 148, 189, 208, 234, 253, 272, 314, 325, 389, 392, 404, 416], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"JSONFormatter": {"executed_lines": [31, 32, 35, 41, 42, 56, 58, 59, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 77, 79, 80, 81, 82, 98, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 119, 121, 122, 123, 140, 141, 142, 143, 144, 160, 162, 163, 166, 167, 168, 169, 170, 172, 173, 174, 175, 180, 182, 183, 184, 201, 202, 220, 222, 223, 224, 225, 226, 228, 246, 248, 249, 251, 265, 267, 268, 270, 284, 293, 304, 305, 306, 309, 310, 312, 323, 335, 336, 339, 340, 343, 344, 345, 350, 351, 352, 353, 355, 358, 359, 362, 363, 366, 367, 370, 371, 372], "summary": {"covered_lines": 108, "num_statements": 126, "percent_covered": 88.46153846153847, "percent_covered_display": "88.46", "missing_lines": 18, "excluded_lines": 0, "num_branches": 56, "num_partial_branches": 3, "covered_branches": 53, "missing_branches": 3}, "missing_lines": [124, 185, 203, 204, 229, 230, 346, 347, 373, 375, 376, 377, 378, 381, 382, 383, 384, 385], "excluded_lines": [], "executed_branches": [[41, -22], [41, 42], [58, 59], [58, 62], [63, 64], [63, 77], [65, 66], [65, 68], [80, 81], [80, 82], [100, 101], [100, 104], [105, 106], [105, 119], [107, 108], [107, 110], [122, 123], [162, 163], [162, 166], [167, 168], [167, 180], [169, 170], [169, 172], [183, 184], [223, 224], [223, 225], [225, 226], [225, 228], [248, 249], [248, 251], [267, 268], [267, 270], [304, 305], [304, 309], [305, 304], [305, 306], [309, 310], [309, 312], [335, 336], [335, 339], [339, 340], [339, 343], [343, 344], [343, 350], [350, 351], [350, 358], [358, 359], [358, 362], [362, 363], [362, 366], [366, 367], [366, 370], [370, 371]], "missing_branches": [[122, 124], [183, 185], [370, 381]]}, "": {"executed_lines": [1, 8, 9, 11, 12, 15, 16, 22, 44, 86, 128, 148, 189, 208, 234, 253, 272, 314, 325, 389, 392, 401, 404, 413, 416, 425], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/formatters/pretty_formatter.py": {"executed_lines": [1, 8, 9, 10, 12, 14, 15, 18, 19, 25, 34, 35, 38, 39, 40, 41, 43, 55, 56, 57, 59, 62, 63, 64, 66, 68, 69, 72, 73, 74, 77, 78, 79, 82, 85, 89, 90, 94, 95, 99, 100, 105, 106, 107, 109, 112, 113, 114, 115, 117, 118, 121, 122, 123, 128, 130, 132, 133, 137, 149, 150, 151, 153, 156, 157, 158, 160, 161, 163, 164, 165, 167, 169, 170, 175, 187, 188, 189, 191, 192, 194, 195, 197, 198, 200, 201, 203, 204, 207, 208, 209, 210, 213, 214, 216, 217, 221, 233, 234, 235, 238, 239, 242, 243, 244, 245, 246, 247, 249, 256, 268, 269, 270, 272, 273, 275, 276, 278, 279, 289, 301, 302, 305, 306, 307, 310, 311, 314, 315, 318, 319, 320, 322, 329, 340, 341, 343, 345, 355, 356, 358, 367, 369, 380, 381, 382, 384, 385, 387, 396, 402, 404, 414, 415, 416, 417, 419, 421, 430, 431, 433, 436, 437, 440, 441, 442, 443, 445, 446, 449, 450, 451, 452, 455, 456, 458, 460, 470, 473, 474, 475, 476, 477, 479, 483, 484, 486, 488, 491, 492, 493, 496, 497, 498, 500, 502, 511, 512, 513, 515, 516, 517, 518, 519, 521, 524, 525, 531, 532, 533, 535, 537, 539, 548, 549, 551, 552, 554, 555, 556, 557, 558, 560, 561, 563, 572, 576, 577, 578, 580, 583, 584, 585, 586, 587, 589, 590, 593, 594, 595, 597, 600, 601, 602, 603, 604, 605, 606, 609, 610, 613, 614, 615, 616, 617, 618, 621, 622, 623, 624, 625, 626, 627, 629, 631, 632, 633, 635, 637, 646, 647, 648, 652, 654, 657, 659, 660, 661, 663, 664, 665, 667, 668, 669, 671, 675, 678, 687, 690, 699, 702, 711], "summary": {"covered_lines": 290, "num_statements": 299, "percent_covered": 96.01873536299766, "percent_covered_display": "96.02", "missing_lines": 9, "excluded_lines": 0, "num_branches": 128, "num_partial_branches": 8, "covered_branches": 120, "missing_branches": 8}, "missing_lines": [212, 251, 252, 281, 283, 284, 324, 325, 573], "excluded_lines": [], "executed_branches": [[56, 57], [56, 59], [66, 68], [66, 128], [78, 79], [78, 82], [105, 106], [105, 121], [112, 113], [112, 121], [117, 112], [117, 118], [121, 66], [121, 122], [150, 151], [150, 153], [160, 161], [160, 167], [163, 160], [163, 164], [188, 189], [188, 191], [191, 192], [191, 194], [194, 195], [194, 197], [197, 198], [197, 200], [200, 201], [200, 203], [203, 204], [203, 207], [209, 210], [234, 235], [234, 238], [238, 239], [238, 242], [243, 244], [243, 247], [269, 270], [269, 272], [272, 273], [272, 275], [275, 276], [275, 278], [278, 279], [310, 311], [314, 315], [318, 319], [318, 322], [319, 318], [319, 320], [340, 341], [340, 343], [381, 382], [381, 384], [414, 415], [414, 416], [416, 417], [416, 419], [430, 431], [430, 433], [440, 441], [441, 442], [441, 445], [449, 450], [449, 455], [455, 456], [455, 458], [474, 475], [474, 479], [483, 484], [483, 486], [491, 492], [496, 497], [496, 500], [515, 516], [515, 537], [516, 517], [516, 519], [519, 521], [519, 524], [524, 525], [524, 535], [531, 515], [531, 532], [532, 533], [548, 549], [548, 551], [554, 555], [554, 560], [572, 576], [577, 578], [577, 580], [584, 585], [584, 593], [586, 584], [586, 587], [594, 595], [594, 597], [601, 602], [601, 606], [613, 614], [613, 635], [615, 616], [615, 633], [617, 618], [617, 621], [621, 622], [621, 623], [623, 624], [623, 626], [626, 627], [626, 629], [659, 660], [659, 663], [663, 664], [663, 667], [667, 668], [667, 671]], "missing_branches": [[209, 212], [278, 281], [310, 314], [314, 318], [440, 449], [491, 496], [532, 531], [572, 573]], "functions": {"PrettyFormatter.__init__": {"executed_lines": [34, 35, 38, 39, 40, 41], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter.format_threads": {"executed_lines": [55, 56, 57, 59, 62, 63, 64, 66, 68, 69, 72, 73, 74, 77, 78, 79, 82, 85, 89, 90, 94, 95, 99, 100, 105, 106, 107, 109, 112, 113, 114, 115, 117, 118, 121, 122, 123, 128, 130, 132, 133], "summary": {"covered_lines": 41, "num_statements": 41, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 0, "covered_branches": 14, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[56, 57], [56, 59], [66, 68], [66, 128], [78, 79], [78, 82], [105, 106], [105, 121], [112, 113], [112, 121], [117, 112], [117, 118], [121, 66], [121, 122]], "missing_branches": []}, "PrettyFormatter.format_comments": {"executed_lines": [149, 150, 151, 153, 156, 157, 158, 160, 161, 163, 164, 165, 167, 169, 170], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[150, 151], [150, 153], [160, 161], [160, 167], [163, 160], [163, 164]], "missing_branches": []}, "PrettyFormatter.format_object": {"executed_lines": [187, 188, 189, 191, 192, 194, 195, 197, 198, 200, 201, 203, 204, 207, 208, 209, 210, 213, 214, 216, 217], "summary": {"covered_lines": 21, "num_statements": 22, "percent_covered": 94.44444444444444, "percent_covered_display": "94.44", "missing_lines": 1, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 1, "covered_branches": 13, "missing_branches": 1}, "missing_lines": [212], "excluded_lines": [], "executed_branches": [[188, 189], [188, 191], [191, 192], [191, 194], [194, 195], [194, 197], [197, 198], [197, 200], [200, 201], [200, 203], [203, 204], [203, 207], [209, 210]], "missing_branches": [[209, 212]]}, "PrettyFormatter.format_array": {"executed_lines": [233, 234, 235, 238, 239, 242, 243, 244, 245, 246, 247, 249], "summary": {"covered_lines": 12, "num_statements": 14, "percent_covered": 90.0, "percent_covered_display": "90.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [251, 252], "excluded_lines": [], "executed_branches": [[234, 235], [234, 238], [238, 239], [238, 242], [243, 244], [243, 247]], "missing_branches": []}, "PrettyFormatter.format_primitive": {"executed_lines": [268, 269, 270, 272, 273, 275, 276, 278, 279], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 80.0, "percent_covered_display": "80.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [281, 283, 284], "excluded_lines": [], "executed_branches": [[269, 270], [269, 272], [272, 273], [272, 275], [275, 276], [275, 278], [278, 279]], "missing_branches": [[278, 281]]}, "PrettyFormatter.format_error": {"executed_lines": [301, 302, 305, 306, 307, 310, 311, 314, 315, 318, 319, 320, 322], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 82.6086956521739, "percent_covered_display": "82.61", "missing_lines": 2, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2}, "missing_lines": [324, 325], "excluded_lines": [], "executed_branches": [[310, 311], [314, 315], [318, 319], [318, 322], [319, 318], [319, 320]], "missing_branches": [[310, 314], [314, 318]]}, "PrettyFormatter._style": {"executed_lines": [340, 341, 343], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[340, 341], [340, 343]], "missing_branches": []}, "PrettyFormatter._strip_ansi_codes": {"executed_lines": [355, 356], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._display_width": {"executed_lines": [367], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._pad_to_width": {"executed_lines": [380, 381, 382, 384, 385], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[381, 382], [381, 384]], "missing_branches": []}, "PrettyFormatter._get_status_color": {"executed_lines": [396, 402], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._get_status_emoji": {"executed_lines": [414, 415, 416, 417, 419], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[414, 415], [414, 416], [416, 417], [416, 419]], "missing_branches": []}, "PrettyFormatter._format_file_context": {"executed_lines": [430, 431, 433, 436, 437, 440, 441, 442, 443, 445, 446, 449, 450, 451, 452, 455, 456, 458], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.42857142857143, "percent_covered_display": "96.43", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[430, 431], [430, 433], [440, 441], [441, 442], [441, 445], [449, 450], [449, 455], [455, 456], [455, 458]], "missing_branches": [[440, 449]]}, "PrettyFormatter._format_comment": {"executed_lines": [470, 473, 474, 475, 476, 477, 479, 483, 484, 486, 488, 491, 492, 493, 496, 497, 498, 500], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.15384615384616, "percent_covered_display": "96.15", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[474, 475], [474, 479], [483, 484], [483, 486], [491, 492], [496, 497], [496, 500]], "missing_branches": [[491, 496]]}, "PrettyFormatter._wrap_comment_content": {"executed_lines": [511, 512, 513, 515, 516, 517, 518, 519, 521, 524, 525, 531, 532, 533, 535, 537], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 96.42857142857143, "percent_covered_display": "96.43", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 1, "covered_branches": 11, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[515, 516], [515, 537], [516, 517], [516, 519], [519, 521], [519, 524], [524, 525], [524, 535], [531, 515], [531, 532], [532, 533]], "missing_branches": [[532, 531]]}, "PrettyFormatter._format_dict": {"executed_lines": [548, 549, 551, 552, 554, 555, 556, 557, 558, 560, 561], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[548, 549], [548, 551], [554, 555], [554, 560]], "missing_branches": []}, "PrettyFormatter._format_table": {"executed_lines": [572, 576, 577, 578, 580, 583, 584, 585, 586, 587, 589, 590, 593, 594, 595, 597, 600, 601, 602, 603, 604, 605, 606, 609, 610, 613, 614, 615, 616, 617, 618, 621, 622, 623, 624, 625, 626, 627, 629, 631, 632, 633, 635], "summary": {"covered_lines": 43, "num_statements": 44, "percent_covered": 97.05882352941177, "percent_covered_display": "97.06", "missing_lines": 1, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [573], "excluded_lines": [], "executed_branches": [[572, 576], [577, 578], [577, 580], [584, 585], [584, 593], [586, 584], [586, 587], [594, 595], [594, 597], [601, 602], [601, 606], [613, 614], [613, 635], [615, 616], [615, 633], [617, 618], [617, 621], [621, 622], [621, 623], [623, 624], [623, 626], [626, 627], [626, 629]], "missing_branches": [[572, 573]]}, "PrettyFormatter._format_summary": {"executed_lines": [646, 647, 648, 652, 654, 657, 659, 660, 661, 663, 664, 665, 667, 668, 669, 671], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[659, 660], [659, 663], [663, 664], [663, 667], [667, 668], [667, 671]], "missing_branches": []}, "format_threads_pretty": {"executed_lines": [687], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_comments_pretty": {"executed_lines": [699], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_object_pretty": {"executed_lines": [711], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 10, 12, 14, 15, 18, 19, 25, 43, 137, 175, 221, 256, 289, 329, 345, 358, 369, 387, 404, 421, 460, 502, 539, 563, 637, 675, 678, 690, 702], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"PrettyFormatter": {"executed_lines": [34, 35, 38, 39, 40, 41, 55, 56, 57, 59, 62, 63, 64, 66, 68, 69, 72, 73, 74, 77, 78, 79, 82, 85, 89, 90, 94, 95, 99, 100, 105, 106, 107, 109, 112, 113, 114, 115, 117, 118, 121, 122, 123, 128, 130, 132, 133, 149, 150, 151, 153, 156, 157, 158, 160, 161, 163, 164, 165, 167, 169, 170, 187, 188, 189, 191, 192, 194, 195, 197, 198, 200, 201, 203, 204, 207, 208, 209, 210, 213, 214, 216, 217, 233, 234, 235, 238, 239, 242, 243, 244, 245, 246, 247, 249, 268, 269, 270, 272, 273, 275, 276, 278, 279, 301, 302, 305, 306, 307, 310, 311, 314, 315, 318, 319, 320, 322, 340, 341, 343, 355, 356, 367, 380, 381, 382, 384, 385, 396, 402, 414, 415, 416, 417, 419, 430, 431, 433, 436, 437, 440, 441, 442, 443, 445, 446, 449, 450, 451, 452, 455, 456, 458, 470, 473, 474, 475, 476, 477, 479, 483, 484, 486, 488, 491, 492, 493, 496, 497, 498, 500, 511, 512, 513, 515, 516, 517, 518, 519, 521, 524, 525, 531, 532, 533, 535, 537, 548, 549, 551, 552, 554, 555, 556, 557, 558, 560, 561, 572, 576, 577, 578, 580, 583, 584, 585, 586, 587, 589, 590, 593, 594, 595, 597, 600, 601, 602, 603, 604, 605, 606, 609, 610, 613, 614, 615, 616, 617, 618, 621, 622, 623, 624, 625, 626, 627, 629, 631, 632, 633, 635, 646, 647, 648, 652, 654, 657, 659, 660, 661, 663, 664, 665, 667, 668, 669, 671], "summary": {"covered_lines": 257, "num_statements": 266, "percent_covered": 95.68527918781726, "percent_covered_display": "95.69", "missing_lines": 9, "excluded_lines": 0, "num_branches": 128, "num_partial_branches": 8, "covered_branches": 120, "missing_branches": 8}, "missing_lines": [212, 251, 252, 281, 283, 284, 324, 325, 573], "excluded_lines": [], "executed_branches": [[56, 57], [56, 59], [66, 68], [66, 128], [78, 79], [78, 82], [105, 106], [105, 121], [112, 113], [112, 121], [117, 112], [117, 118], [121, 66], [121, 122], [150, 151], [150, 153], [160, 161], [160, 167], [163, 160], [163, 164], [188, 189], [188, 191], [191, 192], [191, 194], [194, 195], [194, 197], [197, 198], [197, 200], [200, 201], [200, 203], [203, 204], [203, 207], [209, 210], [234, 235], [234, 238], [238, 239], [238, 242], [243, 244], [243, 247], [269, 270], [269, 272], [272, 273], [272, 275], [275, 276], [275, 278], [278, 279], [310, 311], [314, 315], [318, 319], [318, 322], [319, 318], [319, 320], [340, 341], [340, 343], [381, 382], [381, 384], [414, 415], [414, 416], [416, 417], [416, 419], [430, 431], [430, 433], [440, 441], [441, 442], [441, 445], [449, 450], [449, 455], [455, 456], [455, 458], [474, 475], [474, 479], [483, 484], [483, 486], [491, 492], [496, 497], [496, 500], [515, 516], [515, 537], [516, 517], [516, 519], [519, 521], [519, 524], [524, 525], [524, 535], [531, 515], [531, 532], [532, 533], [548, 549], [548, 551], [554, 555], [554, 560], [572, 576], [577, 578], [577, 580], [584, 585], [584, 593], [586, 584], [586, 587], [594, 595], [594, 597], [601, 602], [601, 606], [613, 614], [613, 635], [615, 616], [615, 633], [617, 618], [617, 621], [621, 622], [621, 623], [623, 624], [623, 626], [626, 627], [626, 629], [659, 660], [659, 663], [663, 664], [663, 667], [667, 668], [667, 671]], "missing_branches": [[209, 212], [278, 281], [310, 314], [314, 318], [440, 449], [491, 496], [532, 531], [572, 573]]}, "": {"executed_lines": [1, 8, 9, 10, 12, 14, 15, 18, 19, 25, 43, 137, 175, 221, 256, 289, 329, 345, 358, 369, 387, 404, 421, 460, 502, 539, 563, 637, 675, 678, 687, 690, 699, 702, 711], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/models/__init__.py": {"executed_lines": [1, 3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1, 3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/models/models.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 11, 23, 24, 25, 27, 29, 30, 38, 39, 40, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 75, 77, 83, 85, 86, 92, 93, 101, 102, 108, 109, 117, 118, 124, 125, 136, 143, 144, 152, 159, 166, 167, 175, 184, 186, 188, 198, 205, 207, 224, 225, 238, 246, 247, 248, 256, 257, 258, 259, 266, 267, 268, 269, 277, 278, 279, 280, 283, 286, 303, 304, 310, 312, 314, 320, 321, 322, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 351, 353, 359, 361, 368, 369, 377, 384, 385, 391, 392, 404, 411, 412, 420, 427, 428, 436, 443, 450, 451, 458, 460, 470, 479, 480, 501, 502, 514, 516, 525, 533, 534, 535, 547, 548, 549, 550, 557, 558, 559, 560, 568, 569, 593, 595, 604, 606, 612, 613, 614, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 642, 648, 650, 651, 657, 658, 666, 673, 674, 682, 689, 690, 698, 705, 706, 714, 721, 722, 730, 731, 739, 746, 755, 762, 763, 771, 778, 785, 786, 793, 795, 805, 811, 825, 826, 839, 851, 852, 853, 861, 862, 863, 864, 871, 872, 882, 896, 898, 899], "summary": {"covered_lines": 205, "num_statements": 241, "percent_covered": 83.48082595870207, "percent_covered_display": "83.48", "missing_lines": 36, "excluded_lines": 0, "num_branches": 98, "num_partial_branches": 20, "covered_branches": 78, "missing_branches": 20}, "missing_lines": [137, 153, 160, 176, 189, 191, 362, 378, 405, 421, 437, 444, 461, 463, 493, 494, 517, 582, 584, 585, 586, 596, 597, 667, 683, 699, 715, 740, 747, 756, 772, 779, 796, 798, 873, 874], "excluded_lines": [], "executed_branches": [[27, 29], [27, 30], [85, 86], [85, 92], [92, 93], [92, 101], [101, 102], [101, 108], [108, 109], [108, 117], [117, 118], [117, 124], [124, 125], [124, 136], [136, 143], [143, 144], [143, 152], [152, 159], [159, 166], [166, 167], [166, 175], [175, 184], [247, 248], [247, 256], [278, 279], [278, 286], [279, 280], [279, 283], [361, 368], [368, 369], [368, 377], [377, 384], [384, 385], [384, 391], [391, 392], [391, 404], [404, 411], [411, 412], [411, 420], [420, 427], [427, 428], [427, 436], [436, 443], [443, 450], [450, -353], [450, 451], [516, 525], [534, 535], [534, 547], [650, 651], [650, 657], [657, 658], [657, 666], [666, 673], [673, 674], [673, 682], [682, 689], [689, 690], [689, 698], [698, 705], [705, 706], [705, 714], [714, 721], [721, 722], [721, 730], [730, 731], [730, 739], [739, 746], [746, 755], [755, 762], [762, 763], [762, 771], [771, 778], [778, 785], [785, -642], [785, 786], [852, 853], [852, 861]], "missing_branches": [[136, 137], [152, 153], [159, 160], [175, 176], [361, 362], [377, 378], [404, 405], [420, 421], [436, 437], [443, 444], [516, 517], [666, 667], [682, 683], [698, 699], [714, 715], [739, 740], [746, 747], [755, 756], [771, 772], [778, 779]], "functions": {"_parse_datetime": {"executed_lines": [23, 24, 25, 27, 29, 30], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[27, 29], [27, 30]], "missing_branches": []}, "ReviewThread.__post_init__": {"executed_lines": [83, 85, 86, 92, 93, 101, 102, 108, 109, 117, 118, 124, 125, 136, 143, 144, 152, 159, 166, 167, 175, 184, 186, 188], "summary": {"covered_lines": 24, "num_statements": 30, "percent_covered": 81.48148148148148, "percent_covered_display": "81.48", "missing_lines": 6, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 4, "covered_branches": 20, "missing_branches": 4}, "missing_lines": [137, 153, 160, 176, 189, 191], "excluded_lines": [], "executed_branches": [[85, 86], [85, 92], [92, 93], [92, 101], [101, 102], [101, 108], [108, 109], [108, 117], [117, 118], [117, 124], [124, 125], [124, 136], [136, 143], [143, 144], [143, 152], [152, 159], [159, 166], [166, 167], [166, 175], [175, 184]], "missing_branches": [[136, 137], [152, 153], [159, 160], [175, 176]]}, "ReviewThread.to_dict": {"executed_lines": [205, 207], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThread.from_dict": {"executed_lines": [238, 246, 247, 248, 256, 257, 258, 259, 266, 267, 268, 269, 277, 278, 279, 280, 283, 286], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[247, 248], [247, 256], [278, 279], [278, 286], [279, 280], [279, 283]], "missing_branches": []}, "ReviewThread.is_resolved": {"executed_lines": [310], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThread.__str__": {"executed_lines": [314], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "Comment.__post_init__": {"executed_lines": [359, 361, 368, 369, 377, 384, 385, 391, 392, 404, 411, 412, 420, 427, 428, 436, 443, 450, 451, 458, 460], "summary": {"covered_lines": 21, "num_statements": 29, "percent_covered": 73.58490566037736, "percent_covered_display": "73.58", "missing_lines": 8, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 6, "covered_branches": 18, "missing_branches": 6}, "missing_lines": [362, 378, 405, 421, 437, 444, 461, 463], "excluded_lines": [], "executed_branches": [[361, 368], [368, 369], [368, 377], [377, 384], [384, 385], [384, 391], [391, 392], [391, 404], [404, 411], [411, 412], [411, 420], [420, 427], [427, 428], [427, 436], [436, 443], [443, 450], [450, -353], [450, 451]], "missing_branches": [[361, 362], [377, 378], [404, 405], [420, 421], [436, 437], [443, 444]]}, "Comment.to_dict": {"executed_lines": [479, 480], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [493, 494], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "Comment.from_dict": {"executed_lines": [514, 516, 525, 533, 534, 535, 547, 548, 549, 550, 557, 558, 559, 560, 568, 569, 593, 595], "summary": {"covered_lines": 18, "num_statements": 25, "percent_covered": 72.41379310344827, "percent_covered_display": "72.41", "missing_lines": 7, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [517, 582, 584, 585, 586, 596, 597], "excluded_lines": [], "executed_branches": [[516, 525], [534, 535], [534, 547]], "missing_branches": [[516, 517]]}, "Comment.__str__": {"executed_lines": [606], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequest.__post_init__": {"executed_lines": [648, 650, 651, 657, 658, 666, 673, 674, 682, 689, 690, 698, 705, 706, 714, 721, 722, 730, 731, 739, 746, 755, 762, 763, 771, 778, 785, 786, 793, 795], "summary": {"covered_lines": 30, "num_statements": 41, "percent_covered": 74.02597402597402, "percent_covered_display": "74.03", "missing_lines": 11, "excluded_lines": 0, "num_branches": 36, "num_partial_branches": 9, "covered_branches": 27, "missing_branches": 9}, "missing_lines": [667, 683, 699, 715, 740, 747, 756, 772, 779, 796, 798], "excluded_lines": [], "executed_branches": [[650, 651], [650, 657], [657, 658], [657, 666], [666, 673], [673, 674], [673, 682], [682, 689], [689, 690], [689, 698], [698, 705], [705, 706], [705, 714], [714, 721], [721, 722], [721, 730], [730, 731], [730, 739], [739, 746], [746, 755], [755, 762], [762, 763], [762, 771], [771, 778], [778, 785], [785, -642], [785, 786]], "missing_branches": [[666, 667], [682, 683], [698, 699], [714, 715], [739, 740], [746, 747], [755, 756], [771, 772], [778, 779]]}, "PullRequest.to_dict": {"executed_lines": [811], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequest.from_dict": {"executed_lines": [839, 851, 852, 853, 861, 862, 863, 864, 871, 872, 882], "summary": {"covered_lines": 11, "num_statements": 13, "percent_covered": 86.66666666666667, "percent_covered_display": "86.67", "missing_lines": 2, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [873, 874], "excluded_lines": [], "executed_branches": [[852, 853], [852, 861]], "missing_branches": []}, "PullRequest.__str__": {"executed_lines": [898, 899], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 11, 38, 39, 40, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 75, 77, 198, 224, 225, 303, 304, 312, 320, 321, 322, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 351, 353, 470, 501, 502, 604, 612, 613, 614, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 642, 805, 825, 826, 896], "summary": {"covered_lines": 67, "num_statements": 67, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ReviewThread": {"executed_lines": [83, 85, 86, 92, 93, 101, 102, 108, 109, 117, 118, 124, 125, 136, 143, 144, 152, 159, 166, 167, 175, 184, 186, 188, 205, 207, 238, 246, 247, 248, 256, 257, 258, 259, 266, 267, 268, 269, 277, 278, 279, 280, 283, 286, 310, 314], "summary": {"covered_lines": 46, "num_statements": 52, "percent_covered": 87.8048780487805, "percent_covered_display": "87.80", "missing_lines": 6, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 4, "covered_branches": 26, "missing_branches": 4}, "missing_lines": [137, 153, 160, 176, 189, 191], "excluded_lines": [], "executed_branches": [[85, 86], [85, 92], [92, 93], [92, 101], [101, 102], [101, 108], [108, 109], [108, 117], [117, 118], [117, 124], [124, 125], [124, 136], [136, 143], [143, 144], [143, 152], [152, 159], [159, 166], [166, 167], [166, 175], [175, 184], [247, 248], [247, 256], [278, 279], [278, 286], [279, 280], [279, 283]], "missing_branches": [[136, 137], [152, 153], [159, 160], [175, 176]]}, "Comment": {"executed_lines": [359, 361, 368, 369, 377, 384, 385, 391, 392, 404, 411, 412, 420, 427, 428, 436, 443, 450, 451, 458, 460, 479, 480, 514, 516, 525, 533, 534, 535, 547, 548, 549, 550, 557, 558, 559, 560, 568, 569, 593, 595, 606], "summary": {"covered_lines": 42, "num_statements": 59, "percent_covered": 72.41379310344827, "percent_covered_display": "72.41", "missing_lines": 17, "excluded_lines": 0, "num_branches": 28, "num_partial_branches": 7, "covered_branches": 21, "missing_branches": 7}, "missing_lines": [362, 378, 405, 421, 437, 444, 461, 463, 493, 494, 517, 582, 584, 585, 586, 596, 597], "excluded_lines": [], "executed_branches": [[361, 368], [368, 369], [368, 377], [377, 384], [384, 385], [384, 391], [391, 392], [391, 404], [404, 411], [411, 412], [411, 420], [420, 427], [427, 428], [427, 436], [436, 443], [443, 450], [450, -353], [450, 451], [516, 525], [534, 535], [534, 547]], "missing_branches": [[361, 362], [377, 378], [404, 405], [420, 421], [436, 437], [443, 444], [516, 517]]}, "PullRequest": {"executed_lines": [648, 650, 651, 657, 658, 666, 673, 674, 682, 689, 690, 698, 705, 706, 714, 721, 722, 730, 731, 739, 746, 755, 762, 763, 771, 778, 785, 786, 793, 795, 811, 839, 851, 852, 853, 861, 862, 863, 864, 871, 872, 882, 898, 899], "summary": {"covered_lines": 44, "num_statements": 57, "percent_covered": 76.84210526315789, "percent_covered_display": "76.84", "missing_lines": 13, "excluded_lines": 0, "num_branches": 38, "num_partial_branches": 9, "covered_branches": 29, "missing_branches": 9}, "missing_lines": [667, 683, 699, 715, 740, 747, 756, 772, 779, 796, 798, 873, 874], "excluded_lines": [], "executed_branches": [[650, 651], [650, 657], [657, 658], [657, 666], [666, 673], [673, 674], [673, 682], [682, 689], [689, 690], [689, 698], [698, 705], [705, 706], [705, 714], [714, 721], [721, 722], [721, 730], [730, 731], [730, 739], [739, 746], [746, 755], [755, 762], [762, 763], [762, 771], [771, 778], [778, 785], [785, -642], [785, 786], [852, 853], [852, 861]], "missing_branches": [[666, 667], [682, 683], [698, 699], [714, 715], [739, 740], [746, 747], [755, 756], [771, 772], [778, 779]]}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 11, 23, 24, 25, 27, 29, 30, 38, 39, 40, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 75, 77, 198, 224, 225, 303, 304, 312, 320, 321, 322, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 351, 353, 470, 501, 502, 604, 612, 613, 614, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 642, 805, 825, 826, 896], "summary": {"covered_lines": 73, "num_statements": 73, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[27, 29], [27, 30]], "missing_branches": []}}}, "src/toady/parsers/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/parsers/graphql_parser.py": {"executed_lines": [1, 7, 8, 9, 12, 13, 14, 16, 17, 18, 19, 20, 23, 24, 25, 27, 28, 29, 30, 33, 34, 36, 38, 40, 53, 56, 58, 60, 63, 66, 67, 69, 71, 74, 80, 81, 83, 86, 87, 90, 91, 92, 95, 96, 98, 99, 101, 104, 106, 113, 115, 118, 121, 122, 123, 124, 125, 127, 129, 133, 134, 136, 138, 142, 144, 145, 148, 149, 150, 152, 154, 156, 161, 164, 168, 169, 172, 174, 175, 178, 179, 180, 181, 184, 185, 186, 189, 190, 191, 194, 195, 197, 199, 200, 202, 205, 206, 207, 209, 217, 219, 224, 226, 227, 239, 242, 243, 245, 249, 250, 254, 262, 264, 266, 269, 272, 273, 274, 275, 279, 281, 282, 284, 285, 287, 290, 292, 294, 296, 298, 301, 302, 303, 305, 306, 308, 312, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 328, 330, 339, 341, 342, 343, 344, 345, 347, 348, 350, 359, 361, 364, 365, 368, 370, 371, 373, 374], "summary": {"covered_lines": 164, "num_statements": 181, "percent_covered": 87.93774319066148, "percent_covered_display": "87.94", "missing_lines": 17, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 10, "covered_branches": 62, "missing_branches": 14}, "missing_lines": [162, 165, 176, 203, 229, 230, 231, 233, 234, 235, 237, 246, 299, 309, 310, 313, 314], "excluded_lines": [], "executed_branches": [[80, 81], [80, 83], [91, 92], [91, 95], [98, 99], [98, 101], [122, 123], [122, 127], [136, 138], [136, 154], [138, 142], [138, 144], [144, 145], [144, 148], [149, 150], [161, 164], [164, 168], [168, 169], [168, 172], [175, 178], [185, 186], [185, 189], [190, 191], [190, 194], [197, 199], [197, 209], [202, 205], [227, 239], [245, 249], [273, 274], [273, 294], [279, 281], [279, 282], [282, 284], [282, 285], [285, 287], [285, 290], [298, 301], [305, 306], [305, 328], [308, 312], [312, 316], [316, 317], [316, 318], [318, 319], [318, 320], [320, 305], [320, 321], [321, 322], [321, 323], [323, 305], [323, 324], [325, 305], [325, 326], [342, -341], [342, 343], [344, 342], [344, 345], [364, -361], [364, 365], [370, 364], [370, 371]], "missing_branches": [[149, 152], [161, 162], [164, 165], [175, 176], [202, 203], [227, 229], [230, 231], [230, 233], [234, 235], [234, 237], [245, 246], [298, 299], [308, 309], [312, 313]], "functions": {"GraphQLParser.__init__": {"executed_lines": [38], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser.parse": {"executed_lines": [53, 56, 58], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser._clean_query": {"executed_lines": [63, 66, 67, 69], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser._parse_operation": {"executed_lines": [74, 80, 81, 83, 86, 87, 90, 91, 92, 95, 96, 98, 99, 101, 104, 106], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[80, 81], [80, 83], [91, 92], [91, 95], [98, 99], [98, 101]], "missing_branches": []}, "GraphQLParser._parse_variables": {"executed_lines": [115, 118, 121, 122, 123, 124, 125, 127], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[122, 123], [122, 127]], "missing_branches": []}, "GraphQLParser._parse_selections": {"executed_lines": [133, 134, 136, 138, 142, 144, 145, 148, 149, 150, 152, 154], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 95.0, "percent_covered_display": "95.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[136, 138], [136, 154], [138, 142], [138, 144], [144, 145], [144, 148], [149, 150]], "missing_branches": [[149, 152]]}, "GraphQLParser._parse_field": {"executed_lines": [161, 164, 168, 169, 172, 174, 175, 178, 179, 180, 181, 184, 185, 186, 189, 190, 191, 194, 195, 197, 199, 200, 202, 205, 206, 207, 209, 217], "summary": {"covered_lines": 28, "num_statements": 32, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 4, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 4, "covered_branches": 12, "missing_branches": 4}, "missing_lines": [162, 165, 176, 203], "excluded_lines": [], "executed_branches": [[161, 164], [164, 168], [168, 169], [168, 172], [175, 178], [185, 186], [185, 189], [190, 191], [190, 194], [197, 199], [197, 209], [202, 205]], "missing_branches": [[161, 162], [164, 165], [175, 176], [202, 203]]}, "GraphQLParser._parse_inline_fragment": {"executed_lines": [224, 226, 227, 239, 242, 243, 245, 249, 250, 254, 262], "summary": {"covered_lines": 11, "num_statements": 19, "percent_covered": 48.148148148148145, "percent_covered_display": "48.15", "missing_lines": 8, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 6}, "missing_lines": [229, 230, 231, 233, 234, 235, 237, 246], "excluded_lines": [], "executed_branches": [[227, 239], [245, 249]], "missing_branches": [[227, 229], [230, 231], [230, 233], [234, 235], [234, 237], [245, 246]]}, "GraphQLParser._parse_arguments": {"executed_lines": [266, 269, 272, 273, 274, 275, 279, 281, 282, 284, 285, 287, 290, 292, 294], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[273, 274], [273, 294], [279, 281], [279, 282], [282, 284], [282, 285], [285, 287], [285, 290]], "missing_branches": []}, "GraphQLParser._find_matching_brace": {"executed_lines": [298, 301, 302, 303, 305, 306, 308, 312, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 328], "summary": {"covered_lines": 20, "num_statements": 25, "percent_covered": 82.22222222222223, "percent_covered_display": "82.22", "missing_lines": 5, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [299, 309, 310, 313, 314], "excluded_lines": [], "executed_branches": [[298, 301], [305, 306], [305, 328], [308, 312], [312, 316], [316, 317], [316, 318], [318, 319], [318, 320], [320, 305], [320, 321], [321, 322], [321, 323], [323, 305], [323, 324], [325, 305], [325, 326]], "missing_branches": [[298, 299], [308, 309], [312, 313]]}, "GraphQLParser.extract_all_fields": {"executed_lines": [339, 341, 347, 348], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser.extract_all_fields.collect_fields": {"executed_lines": [342, 343, 344, 345], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[342, -341], [342, 343], [344, 342], [344, 345]], "missing_branches": []}, "GraphQLParser.extract_field_paths": {"executed_lines": [359, 361, 373, 374], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser.extract_field_paths.collect_paths": {"executed_lines": [364, 365, 368, 370, 371], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[364, -361], [364, 365], [370, 364], [370, 371]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 12, 13, 14, 16, 17, 18, 19, 20, 23, 24, 25, 27, 28, 29, 30, 33, 34, 36, 40, 60, 71, 113, 129, 156, 219, 264, 296, 330, 350], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GraphQLField": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLOperation": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser": {"executed_lines": [38, 53, 56, 58, 63, 66, 67, 69, 74, 80, 81, 83, 86, 87, 90, 91, 92, 95, 96, 98, 99, 101, 104, 106, 115, 118, 121, 122, 123, 124, 125, 127, 133, 134, 136, 138, 142, 144, 145, 148, 149, 150, 152, 154, 161, 164, 168, 169, 172, 174, 175, 178, 179, 180, 181, 184, 185, 186, 189, 190, 191, 194, 195, 197, 199, 200, 202, 205, 206, 207, 209, 217, 224, 226, 227, 239, 242, 243, 245, 249, 250, 254, 262, 266, 269, 272, 273, 274, 275, 279, 281, 282, 284, 285, 287, 290, 292, 294, 298, 301, 302, 303, 305, 306, 308, 312, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 328, 339, 341, 342, 343, 344, 345, 347, 348, 359, 361, 364, 365, 368, 370, 371, 373, 374], "summary": {"covered_lines": 135, "num_statements": 152, "percent_covered": 86.40350877192982, "percent_covered_display": "86.40", "missing_lines": 17, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 10, "covered_branches": 62, "missing_branches": 14}, "missing_lines": [162, 165, 176, 203, 229, 230, 231, 233, 234, 235, 237, 246, 299, 309, 310, 313, 314], "excluded_lines": [], "executed_branches": [[80, 81], [80, 83], [91, 92], [91, 95], [98, 99], [98, 101], [122, 123], [122, 127], [136, 138], [136, 154], [138, 142], [138, 144], [144, 145], [144, 148], [149, 150], [161, 164], [164, 168], [168, 169], [168, 172], [175, 178], [185, 186], [185, 189], [190, 191], [190, 194], [197, 199], [197, 209], [202, 205], [227, 239], [245, 249], [273, 274], [273, 294], [279, 281], [279, 282], [282, 284], [282, 285], [285, 287], [285, 290], [298, 301], [305, 306], [305, 328], [308, 312], [312, 316], [316, 317], [316, 318], [318, 319], [318, 320], [320, 305], [320, 321], [321, 322], [321, 323], [323, 305], [323, 324], [325, 305], [325, 326], [342, -341], [342, 343], [344, 342], [344, 345], [364, -361], [364, 365], [370, 364], [370, 371]], "missing_branches": [[149, 152], [161, 162], [164, 165], [175, 176], [202, 203], [227, 229], [230, 231], [230, 233], [234, 235], [234, 237], [245, 246], [298, 299], [308, 309], [312, 313]]}, "": {"executed_lines": [1, 7, 8, 9, 12, 13, 14, 16, 17, 18, 19, 20, 23, 24, 25, 27, 28, 29, 30, 33, 34, 36, 40, 60, 71, 113, 129, 156, 219, 264, 296, 330, 350], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/parsers/graphql_queries.py": {"executed_lines": [1, 3, 4, 5, 8, 23, 24, 27, 28, 32, 33, 38, 39, 40, 41, 43, 46, 47, 49, 51, 52, 53, 55, 64, 65, 67, 79, 80, 81, 82, 84, 96, 97, 98, 99, 101, 108, 164, 166, 177, 179, 185, 188, 201, 202, 203, 204, 205, 208, 222, 223, 226, 227, 229, 276, 279, 297, 298, 300, 302, 303, 305, 308, 309, 311, 313, 314, 315, 317, 329, 330, 331, 332, 334, 343, 344, 346, 352, 389, 391, 401, 403, 409, 412, 424, 425, 426, 427], "summary": {"covered_lines": 85, "num_statements": 85, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 18, "num_partial_branches": 0, "covered_branches": 18, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[23, 24], [23, 27], [27, 28], [27, 32], [32, 33], [32, 38], [79, 80], [79, 81], [96, 97], [96, 98], [222, 223], [222, 226], [297, 298], [297, 300], [302, 303], [302, 305], [329, 330], [329, 331]], "missing_branches": [], "functions": {"_validate_cursor": {"executed_lines": [23, 24, 27, 28, 32, 33, 38, 39, 40, 41, 43], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[23, 24], [23, 27], [27, 28], [27, 32], [32, 33], [32, 38]], "missing_branches": []}, "ReviewThreadQueryBuilder.__init__": {"executed_lines": [51, 52, 53], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.include_resolved": {"executed_lines": [64, 65], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.limit": {"executed_lines": [79, 80, 81, 82], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[79, 80], [79, 81]], "missing_branches": []}, "ReviewThreadQueryBuilder.comment_limit": {"executed_lines": [96, 97, 98, 99], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[96, 97], [96, 98]], "missing_branches": []}, "ReviewThreadQueryBuilder.build_query": {"executed_lines": [108, 164], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.build_variables": {"executed_lines": [177], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.should_filter_resolved": {"executed_lines": [185], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "build_review_threads_query": {"executed_lines": [201, 202, 203, 204, 205], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_paginated_query": {"executed_lines": [222, 223, 226, 227, 229, 276], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[222, 223], [222, 226]], "missing_branches": []}, "create_paginated_query_variables": {"executed_lines": [297, 298, 300, 302, 303, 305], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[297, 298], [297, 300], [302, 303], [302, 305]], "missing_branches": []}, "PullRequestQueryBuilder.__init__": {"executed_lines": [313, 314, 315], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.limit": {"executed_lines": [329, 330, 331, 332], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[329, 330], [329, 331]], "missing_branches": []}, "PullRequestQueryBuilder.include_drafts": {"executed_lines": [343, 344], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.build_query": {"executed_lines": [352, 389], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.build_variables": {"executed_lines": [401], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.should_filter_drafts": {"executed_lines": [409], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "build_open_prs_query": {"executed_lines": [424, 425, 426, 427], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 8, 46, 47, 49, 55, 67, 84, 101, 166, 179, 188, 208, 279, 308, 309, 311, 317, 334, 346, 391, 403, 412], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ReviewThreadQueryBuilder": {"executed_lines": [51, 52, 53, 64, 65, 79, 80, 81, 82, 96, 97, 98, 99, 108, 164, 177, 185], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[79, 80], [79, 81], [96, 97], [96, 98]], "missing_branches": []}, "PullRequestQueryBuilder": {"executed_lines": [313, 314, 315, 329, 330, 331, 332, 343, 344, 352, 389, 401, 409], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[329, 330], [329, 331]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 8, 23, 24, 27, 28, 32, 33, 38, 39, 40, 41, 43, 46, 47, 49, 55, 67, 84, 101, 166, 179, 188, 201, 202, 203, 204, 205, 208, 222, 223, 226, 227, 229, 276, 279, 297, 298, 300, 302, 303, 305, 308, 309, 311, 317, 334, 346, 391, 403, 412, 424, 425, 426, 427], "summary": {"covered_lines": 55, "num_statements": 55, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[23, 24], [23, 27], [27, 28], [27, 32], [32, 33], [32, 38], [222, 223], [222, 226], [297, 298], [297, 300], [302, 303], [302, 305]], "missing_branches": []}}}, "src/toady/parsers/parsers.py": {"executed_lines": [1, 3, 5, 10, 11, 14, 15, 17, 19, 21, 36, 38, 41, 42, 44, 45, 52, 53, 54, 55, 56, 57, 59, 66, 68, 75, 83, 95, 97, 100, 102, 105, 106, 107, 115, 116, 117, 119, 120, 121, 135, 136, 137, 138, 139, 140, 141, 154, 155, 156, 157, 158, 159, 160, 162, 179, 181, 197, 212, 214, 215, 216, 223, 226, 227, 236, 237, 247, 248, 249, 252, 253, 254, 255, 256, 262, 263, 264, 265, 275, 277, 291, 293, 312, 321, 322, 325, 326, 327, 328, 330, 345, 347, 350, 353, 355, 363, 364, 366, 386, 401, 403, 406, 407, 409, 418, 419, 420, 422, 425, 426, 428, 430, 437, 439, 441, 442, 449, 457, 469, 471, 472, 477, 478, 481, 482, 483, 484, 491, 492, 502, 516, 518, 535, 536, 538, 539, 552, 553, 560, 561, 562, 563, 564, 568, 573, 579, 586, 587, 596, 604, 605, 606, 613, 614, 621, 622, 623, 630, 638, 640, 641, 654, 662, 663, 664, 665, 666, 670, 675, 688, 689, 698, 706, 707, 708, 715, 716, 723, 725, 726, 738, 739, 746, 755, 756, 757, 764, 766, 767, 779, 787, 788, 789, 790, 798, 799, 800, 807, 808, 815, 817, 818, 830, 838, 839, 840, 841, 848, 850, 851], "summary": {"covered_lines": 208, "num_statements": 280, "percent_covered": 73.42105263157895, "percent_covered_display": "73.42", "missing_lines": 72, "excluded_lines": 0, "num_branches": 100, "num_partial_branches": 13, "covered_branches": 71, "missing_branches": 29}, "missing_lines": [69, 76, 122, 124, 142, 143, 182, 183, 189, 190, 228, 229, 238, 239, 257, 259, 266, 267, 268, 269, 271, 272, 294, 295, 304, 305, 356, 368, 370, 371, 372, 378, 379, 410, 443, 450, 493, 494, 519, 520, 526, 527, 588, 597, 631, 655, 681, 690, 699, 780, 801, 831, 864, 865, 872, 873, 874, 875, 876, 880, 884, 891, 892, 893, 901, 902, 909, 910, 911, 918, 919, 926], "excluded_lines": [], "executed_branches": [[44, 45], [44, 52], [53, 54], [53, 66], [106, 107], [106, 115], [116, 117], [116, 135], [215, 216], [215, 223], [254, 255], [254, 262], [265, 275], [321, 322], [321, 325], [326, 327], [326, 328], [355, 363], [409, 418], [419, 420], [419, 437], [552, 553], [552, 560], [560, 561], [560, 586], [561, 562], [561, 579], [563, 564], [563, 573], [587, 596], [596, 604], [605, 606], [605, 613], [613, 614], [613, 621], [622, 623], [622, 630], [630, 638], [654, 662], [662, 663], [662, 688], [663, 664], [665, 666], [665, 675], [689, 698], [698, 706], [707, 708], [707, 715], [715, 716], [715, 723], [738, 739], [738, 746], [755, 756], [755, 764], [756, 755], [756, 757], [779, 787], [788, 789], [788, 798], [789, 788], [789, 790], [798, 799], [798, 815], [800, 807], [807, 808], [807, 815], [830, 838], [839, 840], [839, 848], [840, 839], [840, 841]], "missing_branches": [[265, 266], [355, 356], [409, 410], [587, 588], [596, 597], [630, 631], [654, 655], [663, 681], [689, 690], [698, 699], [779, 780], [800, 801], [830, 831], [864, 865], [864, 872], [872, 873], [872, 891], [873, 874], [873, 884], [875, 876], [875, 884], [892, 893], [892, 901], [901, 902], [901, 909], [910, 911], [910, 918], [918, 919], [918, 926]], "functions": {"GraphQLResponseParser.__init__": {"executed_lines": [19], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLResponseParser.parse_review_threads_response": {"executed_lines": [36, 38, 41, 42, 44, 45, 52, 53, 54, 55, 56, 57, 59, 66, 68, 75], "summary": {"covered_lines": 16, "num_statements": 18, "percent_covered": 90.9090909090909, "percent_covered_display": "90.91", "missing_lines": 2, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [69, 76], "excluded_lines": [], "executed_branches": [[44, 45], [44, 52], [53, 54], [53, 66]], "missing_branches": []}, "GraphQLResponseParser._parse_single_review_thread": {"executed_lines": [95, 97, 100, 102, 105, 106, 107, 115, 116, 117, 119, 120, 121, 135, 136, 137, 138, 139, 140, 141, 154, 155, 156, 157, 158, 159, 160, 162, 179, 181], "summary": {"covered_lines": 30, "num_statements": 38, "percent_covered": 80.95238095238095, "percent_covered_display": "80.95", "missing_lines": 8, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [122, 124, 142, 143, 182, 183, 189, 190], "excluded_lines": [], "executed_branches": [[106, 107], [106, 115], [116, 117], [116, 135]], "missing_branches": []}, "GraphQLResponseParser._parse_single_comment": {"executed_lines": [212, 214, 215, 216, 223, 226, 227, 236, 237, 247, 248, 249, 252, 253, 254, 255, 256, 262, 263, 264, 265, 275, 277, 291, 293], "summary": {"covered_lines": 25, "num_statements": 41, "percent_covered": 63.829787234042556, "percent_covered_display": "63.83", "missing_lines": 16, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [228, 229, 238, 239, 257, 259, 266, 267, 268, 269, 271, 272, 294, 295, 304, 305], "excluded_lines": [], "executed_branches": [[215, 216], [215, 223], [254, 255], [254, 262], [265, 275]], "missing_branches": [[265, 266]]}, "GraphQLResponseParser._extract_title_from_comment": {"executed_lines": [321, 322, 325, 326, 327, 328], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[321, 322], [321, 325], [326, 327], [326, 328]], "missing_branches": []}, "GraphQLResponseParser.parse_paginated_response": {"executed_lines": [345, 347, 350, 353, 355, 363, 364, 366], "summary": {"covered_lines": 8, "num_statements": 15, "percent_covered": 52.94117647058823, "percent_covered_display": "52.94", "missing_lines": 7, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [356, 368, 370, 371, 372, 378, 379], "excluded_lines": [], "executed_branches": [[355, 363]], "missing_branches": [[355, 356]]}, "GraphQLResponseParser.parse_pull_requests_response": {"executed_lines": [401, 403, 406, 407, 409, 418, 419, 420, 422, 425, 426, 428, 430, 437, 439, 441, 442, 449], "summary": {"covered_lines": 18, "num_statements": 21, "percent_covered": 84.0, "percent_covered_display": "84.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [410, 443, 450], "excluded_lines": [], "executed_branches": [[409, 418], [419, 420], [419, 437]], "missing_branches": [[409, 410]]}, "GraphQLResponseParser._parse_pull_request_data": {"executed_lines": [469, 471, 472, 477, 478, 481, 482, 483, 484, 491, 492, 502, 516, 518], "summary": {"covered_lines": 14, "num_statements": 20, "percent_covered": 70.0, "percent_covered_display": "70.00", "missing_lines": 6, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [493, 494, 519, 520, 526, 527], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ResponseValidator.validate_graphql_response": {"executed_lines": [552, 553, 560, 561, 562, 563, 564, 568, 573, 579, 586, 587, 596, 604, 605, 606, 613, 614, 621, 622, 623, 630, 638], "summary": {"covered_lines": 23, "num_statements": 26, "percent_covered": 86.95652173913044, "percent_covered_display": "86.96", "missing_lines": 3, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [588, 597, 631], "excluded_lines": [], "executed_branches": [[552, 553], [552, 560], [560, 561], [560, 586], [561, 562], [561, 579], [563, 564], [563, 573], [587, 596], [596, 604], [605, 606], [605, 613], [613, 614], [613, 621], [622, 623], [622, 630], [630, 638]], "missing_branches": [[587, 588], [596, 597], [630, 631]]}, "ResponseValidator.validate_graphql_prs_response": {"executed_lines": [654, 662, 663, 664, 665, 666, 670, 675, 688, 689, 698, 706, 707, 708, 715, 716, 723], "summary": {"covered_lines": 17, "num_statements": 21, "percent_covered": 78.37837837837837, "percent_covered_display": "78.38", "missing_lines": 4, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 4, "covered_branches": 12, "missing_branches": 4}, "missing_lines": [655, 681, 690, 699], "excluded_lines": [], "executed_branches": [[654, 662], [662, 663], [662, 688], [663, 664], [665, 666], [665, 675], [689, 698], [698, 706], [707, 708], [707, 715], [715, 716], [715, 723]], "missing_branches": [[654, 655], [663, 681], [689, 690], [698, 699]]}, "ResponseValidator.validate_pull_request_data": {"executed_lines": [738, 739, 746, 755, 756, 757, 764], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[738, 739], [738, 746], [755, 756], [755, 764], [756, 755], [756, 757]], "missing_branches": []}, "ResponseValidator.validate_review_thread_data": {"executed_lines": [779, 787, 788, 789, 790, 798, 799, 800, 807, 808, 815], "summary": {"covered_lines": 11, "num_statements": 13, "percent_covered": 84.0, "percent_covered_display": "84.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 2}, "missing_lines": [780, 801], "excluded_lines": [], "executed_branches": [[779, 787], [788, 789], [788, 798], [789, 788], [789, 790], [798, 799], [798, 815], [800, 807], [807, 808], [807, 815]], "missing_branches": [[779, 780], [800, 801]]}, "ResponseValidator.validate_comment_data": {"executed_lines": [830, 838, 839, 840, 841, 848], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 84.61538461538461, "percent_covered_display": "84.62", "missing_lines": 1, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [831], "excluded_lines": [], "executed_branches": [[830, 838], [839, 840], [839, 848], [840, 839], [840, 841]], "missing_branches": [[830, 831]]}, "ResponseValidator.validate_pull_requests_response": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 20, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 20, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 16}, "missing_lines": [864, 865, 872, 873, 874, 875, 876, 880, 884, 891, 892, 893, 901, 902, 909, 910, 911, 918, 919, 926], "excluded_lines": [], "executed_branches": [], "missing_branches": [[864, 865], [864, 872], [872, 873], [872, 891], [873, 874], [873, 884], [875, 876], [875, 884], [892, 893], [892, 901], [901, 902], [901, 909], [910, 911], [910, 918], [918, 919], [918, 926]]}, "": {"executed_lines": [1, 3, 5, 10, 11, 14, 15, 17, 21, 83, 197, 312, 330, 386, 457, 535, 536, 538, 539, 640, 641, 725, 726, 766, 767, 817, 818, 850, 851], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GraphQLResponseParser": {"executed_lines": [19, 36, 38, 41, 42, 44, 45, 52, 53, 54, 55, 56, 57, 59, 66, 68, 75, 95, 97, 100, 102, 105, 106, 107, 115, 116, 117, 119, 120, 121, 135, 136, 137, 138, 139, 140, 141, 154, 155, 156, 157, 158, 159, 160, 162, 179, 181, 212, 214, 215, 216, 223, 226, 227, 236, 237, 247, 248, 249, 252, 253, 254, 255, 256, 262, 263, 264, 265, 275, 277, 291, 293, 321, 322, 325, 326, 327, 328, 345, 347, 350, 353, 355, 363, 364, 366, 401, 403, 406, 407, 409, 418, 419, 420, 422, 425, 426, 428, 430, 437, 439, 441, 442, 449, 469, 471, 472, 477, 478, 481, 482, 483, 484, 491, 492, 502, 516, 518], "summary": {"covered_lines": 118, "num_statements": 160, "percent_covered": 75.54347826086956, "percent_covered_display": "75.54", "missing_lines": 42, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 3, "covered_branches": 21, "missing_branches": 3}, "missing_lines": [69, 76, 122, 124, 142, 143, 182, 183, 189, 190, 228, 229, 238, 239, 257, 259, 266, 267, 268, 269, 271, 272, 294, 295, 304, 305, 356, 368, 370, 371, 372, 378, 379, 410, 443, 450, 493, 494, 519, 520, 526, 527], "excluded_lines": [], "executed_branches": [[44, 45], [44, 52], [53, 54], [53, 66], [106, 107], [106, 115], [116, 117], [116, 135], [215, 216], [215, 223], [254, 255], [254, 262], [265, 275], [321, 322], [321, 325], [326, 327], [326, 328], [355, 363], [409, 418], [419, 420], [419, 437]], "missing_branches": [[265, 266], [355, 356], [409, 410]]}, "ResponseValidator": {"executed_lines": [552, 553, 560, 561, 562, 563, 564, 568, 573, 579, 586, 587, 596, 604, 605, 606, 613, 614, 621, 622, 623, 630, 638, 654, 662, 663, 664, 665, 666, 670, 675, 688, 689, 698, 706, 707, 708, 715, 716, 723, 738, 739, 746, 755, 756, 757, 764, 779, 787, 788, 789, 790, 798, 799, 800, 807, 808, 815, 830, 838, 839, 840, 841, 848], "summary": {"covered_lines": 64, "num_statements": 94, "percent_covered": 67.05882352941177, "percent_covered_display": "67.06", "missing_lines": 30, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 10, "covered_branches": 50, "missing_branches": 26}, "missing_lines": [588, 597, 631, 655, 681, 690, 699, 780, 801, 831, 864, 865, 872, 873, 874, 875, 876, 880, 884, 891, 892, 893, 901, 902, 909, 910, 911, 918, 919, 926], "excluded_lines": [], "executed_branches": [[552, 553], [552, 560], [560, 561], [560, 586], [561, 562], [561, 579], [563, 564], [563, 573], [587, 596], [596, 604], [605, 606], [605, 613], [613, 614], [613, 621], [622, 623], [622, 630], [630, 638], [654, 662], [662, 663], [662, 688], [663, 664], [665, 666], [665, 675], [689, 698], [698, 706], [707, 708], [707, 715], [715, 716], [715, 723], [738, 739], [738, 746], [755, 756], [755, 764], [756, 755], [756, 757], [779, 787], [788, 789], [788, 798], [789, 788], [789, 790], [798, 799], [798, 815], [800, 807], [807, 808], [807, 815], [830, 838], [839, 840], [839, 848], [840, 839], [840, 841]], "missing_branches": [[587, 588], [596, 597], [630, 631], [654, 655], [663, 681], [689, 690], [698, 699], [779, 780], [800, 801], [830, 831], [864, 865], [864, 872], [872, 873], [872, 891], [873, 874], [873, 884], [875, 876], [875, 884], [892, 893], [892, 901], [901, 902], [901, 909], [910, 911], [910, 918], [918, 919], [918, 926]]}, "": {"executed_lines": [1, 3, 5, 10, 11, 14, 15, 17, 21, 83, 197, 312, 330, 386, 457, 535, 536, 538, 539, 640, 641, 725, 726, 766, 767, 817, 818, 850, 851], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/fetch_service.py": {"executed_lines": [1, 3, 5, 6, 10, 11, 12, 15, 16, 18, 21, 22, 24, 35, 36, 37, 43, 44, 45, 47, 72, 74, 79, 80, 83, 86, 89, 90, 93, 95, 97, 98, 100, 102, 111, 112, 113, 118, 119, 120, 122, 124, 146, 149, 157, 180, 182, 185, 186, 189, 192, 195, 196, 199, 201, 203, 204, 206, 208, 228, 231, 238, 263, 265, 271, 273, 274, 276, 278, 279, 280, 284, 285, 286, 287, 289, 291, 292, 294, 296, 325, 327, 329, 332, 337, 339, 342, 344, 347, 353, 355, 357, 358, 360], "summary": {"covered_lines": 91, "num_statements": 93, "percent_covered": 96.7479674796748, "percent_covered_display": "96.75", "missing_lines": 2, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 2, "covered_branches": 28, "missing_branches": 2}, "missing_lines": [38, 343], "excluded_lines": [], "executed_branches": [[37, 43], [89, 90], [89, 93], [97, 98], [97, 100], [112, 113], [112, 118], [119, 120], [119, 122], [195, 196], [195, 199], [203, 204], [203, 206], [271, 273], [271, 276], [276, 278], [276, 284], [285, 286], [285, 287], [291, 292], [291, 294], [327, 329], [327, 332], [337, 339], [337, 342], [342, 344], [357, 358], [357, 360]], "missing_branches": [[37, 38], [342, 343]], "functions": {"FetchService.__init__": {"executed_lines": [35, 36, 37, 43, 44, 45], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 77.77777777777777, "percent_covered_display": "77.78", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [38], "excluded_lines": [], "executed_branches": [[37, 43]], "missing_branches": [[37, 38]]}, "FetchService.fetch_review_threads": {"executed_lines": [72, 74, 79, 80, 83, 86, 89, 90, 93, 95, 97, 98, 100], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[89, 90], [89, 93], [97, 98], [97, 100]], "missing_branches": []}, "FetchService._get_repository_info": {"executed_lines": [111, 112, 113, 118, 119, 120, 122], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[112, 113], [112, 118], [119, 120], [119, 122]], "missing_branches": []}, "FetchService.fetch_review_threads_from_current_repo": {"executed_lines": [146, 149], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FetchService.fetch_open_pull_requests": {"executed_lines": [180, 182, 185, 186, 189, 192, 195, 196, 199, 201, 203, 204, 206], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[195, 196], [195, 199], [203, 204], [203, 206]], "missing_branches": []}, "FetchService.fetch_open_pull_requests_from_current_repo": {"executed_lines": [228, 231], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FetchService.select_pr_interactively": {"executed_lines": [263, 265, 271, 273, 274, 276, 278, 279, 280, 284, 285, 286, 287, 289, 291, 292, 294], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[271, 273], [271, 276], [276, 278], [276, 284], [285, 286], [285, 287], [291, 292], [291, 294]], "missing_branches": []}, "FetchService.fetch_review_threads_with_pr_selection": {"executed_lines": [325, 327, 329, 332, 337, 339, 342, 344, 347, 353, 355, 357, 358, 360], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 91.30434782608695, "percent_covered_display": "91.30", "missing_lines": 1, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [343], "excluded_lines": [], "executed_branches": [[327, 329], [327, 332], [337, 339], [337, 342], [342, 344], [357, 358], [357, 360]], "missing_branches": [[342, 343]]}, "": {"executed_lines": [1, 3, 5, 6, 10, 11, 12, 15, 16, 18, 21, 22, 24, 47, 102, 124, 157, 208, 238, 296], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"FetchServiceError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FetchService": {"executed_lines": [35, 36, 37, 43, 44, 45, 72, 74, 79, 80, 83, 86, 89, 90, 93, 95, 97, 98, 100, 111, 112, 113, 118, 119, 120, 122, 146, 149, 180, 182, 185, 186, 189, 192, 195, 196, 199, 201, 203, 204, 206, 228, 231, 263, 265, 271, 273, 274, 276, 278, 279, 280, 284, 285, 286, 287, 289, 291, 292, 294, 325, 327, 329, 332, 337, 339, 342, 344, 347, 353, 355, 357, 358, 360], "summary": {"covered_lines": 74, "num_statements": 76, "percent_covered": 96.22641509433963, "percent_covered_display": "96.23", "missing_lines": 2, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 2, "covered_branches": 28, "missing_branches": 2}, "missing_lines": [38, 343], "excluded_lines": [], "executed_branches": [[37, 43], [89, 90], [89, 93], [97, 98], [97, 100], [112, 113], [112, 118], [119, 120], [119, 122], [195, 196], [195, 199], [203, 204], [203, 206], [271, 273], [271, 276], [276, 278], [276, 284], [285, 286], [285, 287], [291, 292], [291, 294], [327, 329], [327, 332], [337, 339], [337, 342], [342, 344], [357, 358], [357, 360]], "missing_branches": [[37, 38], [342, 343]]}, "": {"executed_lines": [1, 3, 5, 6, 10, 11, 12, 15, 16, 18, 21, 22, 24, 47, 102, 124, 157, 208, 238, 296], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/github_service.py": {"executed_lines": [1, 3, 4, 5, 8, 34, 63, 80, 98, 99, 101, 104, 105, 107, 110, 111, 113, 116, 117, 119, 122, 123, 125, 128, 129, 131, 134, 135, 137, 146, 147, 149, 150, 152, 158, 159, 165, 166, 167, 169, 178, 179, 185, 186, 191, 192, 193, 195, 196, 197, 201, 207, 208, 214, 215, 216, 218, 230, 231, 232, 235, 236, 239, 240, 241, 243, 245, 262, 263, 265, 267, 268, 277, 283, 287, 292, 296, 301, 302, 304, 306, 307, 310, 311, 315, 329, 331, 332, 333, 334, 336, 346, 347, 348, 349, 350, 351, 352, 354, 373, 377, 378, 379, 381, 383, 384, 387, 388, 391, 395, 396, 397, 399, 411, 414, 419, 420, 421, 422, 424, 425, 427, 429, 443, 444, 445, 446, 448, 449, 451, 452, 454, 456, 467, 468, 479, 480, 481, 483, 500, 501, 502, 503, 539, 567, 591, 645], "summary": {"covered_lines": 139, "num_statements": 214, "percent_covered": 61.510791366906474, "percent_covered_display": "61.51", "missing_lines": 75, "excluded_lines": 0, "num_branches": 64, "num_partial_branches": 2, "covered_branches": 32, "missing_branches": 32}, "missing_lines": [278, 505, 506, 509, 511, 513, 515, 516, 518, 519, 522, 524, 527, 528, 530, 531, 536, 537, 553, 554, 556, 559, 561, 563, 564, 565, 577, 578, 583, 584, 585, 586, 589, 605, 606, 618, 619, 621, 622, 625, 629, 630, 631, 633, 634, 635, 637, 639, 641, 642, 643, 666, 667, 668, 669, 670, 671, 674, 677, 678, 679, 682, 685, 686, 687, 688, 690, 691, 694, 695, 697, 699, 700, 703, 704], "excluded_lines": [], "executed_branches": [[146, 147], [146, 149], [185, 186], [185, 191], [191, 192], [191, 195], [192, 191], [192, 193], [231, 232], [231, 235], [262, 263], [262, 265], [277, 283], [283, 287], [283, 292], [292, 296], [292, 301], [301, 302], [301, 304], [377, 378], [377, 381], [378, 379], [378, 381], [387, 388], [387, 395], [419, 420], [419, 427], [421, 419], [421, 422], [500, 501], [500, 502], [502, 503]], "missing_branches": [[277, 278], [502, 505], [511, 513], [511, 522], [527, 528], [527, 530], [530, 531], [530, 536], [553, 554], [553, 556], [577, 578], [577, 583], [583, 584], [583, 585], [585, 586], [585, 589], [621, 622], [621, 629], [630, 631], [630, 633], [634, 635], [634, 637], [666, 667], [666, 668], [668, 669], [668, 670], [670, 671], [670, 674], [687, 688], [687, 690], [694, 695], [694, 697]], "functions": {"GitHubService.__init__": {"executed_lines": [146, 147, 149, 150], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[146, 147], [146, 149]], "missing_branches": []}, "GitHubService.check_gh_installation": {"executed_lines": [158, 159, 165, 166, 167], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.get_gh_version": {"executed_lines": [178, 179, 185, 186, 191, 192, 193, 195, 196, 197], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[185, 186], [185, 191], [191, 192], [191, 195], [192, 191], [192, 193]], "missing_branches": []}, "GitHubService.check_authentication": {"executed_lines": [207, 208, 214, 215, 216], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.validate_version_compatibility": {"executed_lines": [230, 231, 232, 235, 236, 239, 240, 241, 243], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[231, 232], [231, 235]], "missing_branches": []}, "GitHubService.run_gh_command": {"executed_lines": [262, 263, 265, 267, 268, 277, 283, 287, 292, 296, 301, 302, 304, 306, 307, 310, 311], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 92.85714285714286, "percent_covered_display": "92.86", "missing_lines": 1, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [278], "excluded_lines": [], "executed_branches": [[262, 263], [262, 265], [277, 283], [283, 287], [283, 292], [292, 296], [292, 301], [301, 302], [301, 304]], "missing_branches": [[277, 278]]}, "GitHubService.get_json_output": {"executed_lines": [329, 331, 332, 333, 334], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.get_current_repo": {"executed_lines": [346, 347, 348, 349, 350, 351, 352], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.execute_graphql_query": {"executed_lines": [373, 377, 378, 379, 381, 383, 384, 387, 388, 391, 395, 396, 397], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[377, 378], [377, 381], [378, 379], [378, 381], [387, 388], [387, 395]], "missing_branches": []}, "GitHubService.get_repo_info_from_url": {"executed_lines": [411, 414, 419, 420, 421, 422, 424, 425, 427], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[419, 420], [419, 427], [421, 419], [421, 422]], "missing_branches": []}, "GitHubService.validate_repository_access": {"executed_lines": [443, 444, 445, 446, 448, 449, 451, 452, 454], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.check_pr_exists": {"executed_lines": [467, 468, 479, 480, 481], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.post_reply": {"executed_lines": [500, 501, 502, 503], "summary": {"covered_lines": 4, "num_statements": 21, "percent_covered": 22.580645161290324, "percent_covered_display": "22.58", "missing_lines": 17, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 7}, "missing_lines": [505, 506, 509, 511, 513, 515, 516, 518, 519, 522, 524, 527, 528, 530, 531, 536, 537], "excluded_lines": [], "executed_branches": [[500, 501], [500, 502], [502, 503]], "missing_branches": [[502, 505], [511, 513], [511, 522], [527, 528], [527, 530], [530, 531], [530, 536]]}, "GitHubService.resolve_thread": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 8, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [553, 554, 556, 559, 561, 563, 564, 565], "excluded_lines": [], "executed_branches": [], "missing_branches": [[553, 554], [553, 556]]}, "GitHubService._determine_reply_strategy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 7, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6}, "missing_lines": [577, 578, 583, 584, 585, 586, 589], "excluded_lines": [], "executed_branches": [], "missing_branches": [[577, 578], [577, 583], [583, 584], [583, 585], [585, 586], [585, 589]]}, "GitHubService._get_review_id_for_comment": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 18, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6}, "missing_lines": [605, 606, 618, 619, 621, 622, 625, 629, 630, 631, 633, 634, 635, 637, 639, 641, 642, 643], "excluded_lines": [], "executed_branches": [], "missing_branches": [[621, 622], [621, 629], [630, 631], [630, 633], [634, 635], [634, 637]]}, "GitHubService.fetch_open_pull_requests": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 24, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 24, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 10}, "missing_lines": [666, 667, 668, 669, 670, 671, 674, 677, 678, 679, 682, 685, 686, 687, 688, 690, 691, 694, 695, 697, 699, 700, 703, 704], "excluded_lines": [], "executed_branches": [], "missing_branches": [[666, 667], [666, 668], [668, 669], [668, 670], [670, 671], [670, 674], [687, 688], [687, 690], [694, 695], [694, 697]]}, "": {"executed_lines": [1, 3, 4, 5, 8, 34, 63, 80, 98, 99, 101, 104, 105, 107, 110, 111, 113, 116, 117, 119, 122, 123, 125, 128, 129, 131, 134, 135, 137, 152, 169, 201, 218, 245, 315, 336, 354, 399, 429, 456, 483, 539, 567, 591, 645], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GitHubServiceError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubCLINotFoundError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAuthenticationError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAPIError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubTimeoutError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubRateLimitError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService": {"executed_lines": [146, 147, 149, 150, 158, 159, 165, 166, 167, 178, 179, 185, 186, 191, 192, 193, 195, 196, 197, 207, 208, 214, 215, 216, 230, 231, 232, 235, 236, 239, 240, 241, 243, 262, 263, 265, 267, 268, 277, 283, 287, 292, 296, 301, 302, 304, 306, 307, 310, 311, 329, 331, 332, 333, 334, 346, 347, 348, 349, 350, 351, 352, 373, 377, 378, 379, 381, 383, 384, 387, 388, 391, 395, 396, 397, 411, 414, 419, 420, 421, 422, 424, 425, 427, 443, 444, 445, 446, 448, 449, 451, 452, 454, 467, 468, 479, 480, 481, 500, 501, 502, 503], "summary": {"covered_lines": 102, "num_statements": 177, "percent_covered": 55.601659751037346, "percent_covered_display": "55.60", "missing_lines": 75, "excluded_lines": 0, "num_branches": 64, "num_partial_branches": 2, "covered_branches": 32, "missing_branches": 32}, "missing_lines": [278, 505, 506, 509, 511, 513, 515, 516, 518, 519, 522, 524, 527, 528, 530, 531, 536, 537, 553, 554, 556, 559, 561, 563, 564, 565, 577, 578, 583, 584, 585, 586, 589, 605, 606, 618, 619, 621, 622, 625, 629, 630, 631, 633, 634, 635, 637, 639, 641, 642, 643, 666, 667, 668, 669, 670, 671, 674, 677, 678, 679, 682, 685, 686, 687, 688, 690, 691, 694, 695, 697, 699, 700, 703, 704], "excluded_lines": [], "executed_branches": [[146, 147], [146, 149], [185, 186], [185, 191], [191, 192], [191, 195], [192, 191], [192, 193], [231, 232], [231, 235], [262, 263], [262, 265], [277, 283], [283, 287], [283, 292], [292, 296], [292, 301], [301, 302], [301, 304], [377, 378], [377, 381], [378, 379], [378, 381], [387, 388], [387, 395], [419, 420], [419, 427], [421, 419], [421, 422], [500, 501], [500, 502], [502, 503]], "missing_branches": [[277, 278], [502, 505], [511, 513], [511, 522], [527, 528], [527, 530], [530, 531], [530, 536], [553, 554], [553, 556], [577, 578], [577, 583], [583, 584], [583, 585], [585, 586], [585, 589], [621, 622], [621, 629], [630, 631], [630, 633], [634, 635], [634, 637], [666, 667], [666, 668], [668, 669], [668, 670], [670, 671], [670, 674], [687, 688], [687, 690], [694, 695], [694, 697]]}, "": {"executed_lines": [1, 3, 4, 5, 8, 34, 63, 80, 98, 99, 101, 104, 105, 107, 110, 111, 113, 116, 117, 119, 122, 123, 125, 128, 129, 131, 134, 135, 137, 152, 169, 201, 218, 245, 315, 336, 354, 399, 429, 456, 483, 539, 567, 591, 645], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/pr_selection.py": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 14, 17, 18, 20, 22, 24, 41, 42, 50, 51, 54, 55, 58, 59, 62, 67, 73, 78, 88, 92, 94, 107, 110, 111, 113, 115, 118, 119, 120, 123, 127, 132, 135, 136, 143, 144, 147, 148, 149, 150, 156, 157, 163, 164, 168, 170, 172, 173, 174, 175, 178, 193, 194, 201, 202, 210, 211, 212, 222], "summary": {"covered_lines": 63, "num_statements": 64, "percent_covered": 97.72727272727273, "percent_covered_display": "97.73", "missing_lines": 1, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [176], "excluded_lines": [], "executed_branches": [[41, 42], [41, 50], [50, 51], [50, 54], [54, 55], [54, 58], [58, 59], [58, 62], [113, 115], [113, 132], [119, 120], [119, 123], [143, 144], [143, 147], [156, 157], [156, 163], [174, 175], [193, 194], [193, 201], [201, 202], [201, 210], [211, 212], [211, 222]], "missing_branches": [[174, 176]], "functions": {"PRSelector.__init__": {"executed_lines": [22], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector.select_pull_request": {"executed_lines": [41, 42, 50, 51, 54, 55, 58, 59, 62], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[41, 42], [41, 50], [50, 51], [50, 54], [54, 55], [54, 58], [58, 59], [58, 62]], "missing_branches": []}, "PRSelector._handle_no_prs": {"executed_lines": [73], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector._handle_single_pr": {"executed_lines": [88, 92], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector._handle_multiple_prs": {"executed_lines": [107, 110, 111, 113, 115, 118, 119, 120, 123, 127, 132, 135, 136, 143, 144, 147, 148, 149, 150, 156, 157, 163, 164, 168, 170, 172, 173, 174, 175], "summary": {"covered_lines": 29, "num_statements": 30, "percent_covered": 95.0, "percent_covered_display": "95.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [176], "excluded_lines": [], "executed_branches": [[113, 115], [113, 132], [119, 120], [119, 123], [143, 144], [143, 147], [156, 157], [156, 163], [174, 175]], "missing_branches": [[174, 176]]}, "PRSelector.validate_pr_exists": {"executed_lines": [193, 194, 201, 202, 210, 211, 212, 222], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[193, 194], [193, 201], [201, 202], [201, 210], [211, 212], [211, 222]], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 14, 17, 18, 20, 24, 67, 78, 94, 178], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"PRSelectionError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector": {"executed_lines": [22, 41, 42, 50, 51, 54, 55, 58, 59, 62, 73, 88, 92, 107, 110, 111, 113, 115, 118, 119, 120, 123, 127, 132, 135, 136, 143, 144, 147, 148, 149, 150, 156, 157, 163, 164, 168, 170, 172, 173, 174, 175, 193, 194, 201, 202, 210, 211, 212, 222], "summary": {"covered_lines": 50, "num_statements": 51, "percent_covered": 97.33333333333333, "percent_covered_display": "97.33", "missing_lines": 1, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [176], "excluded_lines": [], "executed_branches": [[41, 42], [41, 50], [50, 51], [50, 54], [54, 55], [54, 58], [58, 59], [58, 62], [113, 115], [113, 132], [119, 120], [119, 123], [143, 144], [143, 147], [156, 157], [156, 163], [174, 175], [193, 194], [193, 201], [201, 202], [201, 210], [211, 212], [211, 222]], "missing_branches": [[174, 176]]}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 14, 17, 18, 20, 24, 67, 78, 94, 178], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/pr_selector.py": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 18, 25, 26, 27, 29, 41, 42, 44, 46, 49, 60, 62, 75, 76, 77, 78, 79, 82, 85, 87, 93, 95, 96, 97, 100, 101, 104, 105, 106, 109, 110, 111, 112, 117, 121, 125, 129, 131, 140, 141, 143, 144, 149, 152, 153, 154, 157, 158, 159, 160, 161, 165, 171, 174, 175, 176, 177, 183, 186, 189, 194, 195, 197, 199, 200, 201, 202, 204, 207, 211, 212, 213, 215, 218, 219, 221, 228, 232, 233, 234, 236, 237, 239, 240, 241, 244, 245, 247, 254, 255, 257, 258, 260, 262, 263, 265, 268, 274], "summary": {"covered_lines": 102, "num_statements": 106, "percent_covered": 94.44444444444444, "percent_covered_display": "94.44", "missing_lines": 4, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [52, 57, 208, 229], "excluded_lines": [], "executed_branches": [[41, 42], [41, 44], [44, 46], [44, 49], [49, 60], [93, -87], [93, 95], [105, 106], [105, 109], [110, 111], [110, 121], [152, 153], [152, 157], [174, 175], [174, 186], [207, 211], [228, 232]], "missing_branches": [[49, 52], [207, 208], [228, 229]], "functions": {"PRSelector.__init__": {"executed_lines": [25, 26, 27], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector.select_pr": {"executed_lines": [41, 42, 44, 46, 49, 60], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 78.57142857142857, "percent_covered_display": "78.57", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [52, 57], "excluded_lines": [], "executed_branches": [[41, 42], [41, 44], [44, 46], [44, 49], [49, 60]], "missing_branches": [[49, 52]]}, "PRSelector._show_pr_selection_menu": {"executed_lines": [75, 76, 77, 78, 79, 82, 85], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector._display_pr_list": {"executed_lines": [93, 95, 96, 97, 100, 101, 104, 105, 106, 109, 110, 111, 112, 117, 121, 125, 129], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[93, -87], [93, 95], [105, 106], [105, 109], [110, 111], [110, 121]], "missing_branches": []}, "PRSelector._prompt_for_selection": {"executed_lines": [140, 141, 143, 144, 149, 152, 153, 154, 157, 158, 159, 160, 161, 165, 171, 174, 175, 176, 177, 183, 186, 189, 194, 195, 197, 199, 200, 201, 202], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[152, 153], [152, 157], [174, 175], [174, 186]], "missing_branches": []}, "PRSelector.display_no_prs_message": {"executed_lines": [207, 211, 212, 213, 215, 218, 219], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 80.0, "percent_covered_display": "80.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [208], "excluded_lines": [], "executed_branches": [[207, 211]], "missing_branches": [[207, 208]]}, "PRSelector.display_auto_selected_pr": {"executed_lines": [228, 232, 233, 234, 236, 237, 239, 240, 241], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [229], "excluded_lines": [], "executed_branches": [[228, 232]], "missing_branches": [[228, 229]]}, "PRSelectionResult.__init__": {"executed_lines": [254, 255], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelectionResult.has_selection": {"executed_lines": [260], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelectionResult.should_continue": {"executed_lines": [265], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_pr_selector": {"executed_lines": [274], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 18, 29, 62, 87, 131, 204, 221, 244, 245, 247, 257, 258, 262, 263, 268], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"PRSelector": {"executed_lines": [25, 26, 27, 41, 42, 44, 46, 49, 60, 75, 76, 77, 78, 79, 82, 85, 93, 95, 96, 97, 100, 101, 104, 105, 106, 109, 110, 111, 112, 117, 121, 125, 129, 140, 141, 143, 144, 149, 152, 153, 154, 157, 158, 159, 160, 161, 165, 171, 174, 175, 176, 177, 183, 186, 189, 194, 195, 197, 199, 200, 201, 202, 207, 211, 212, 213, 215, 218, 219, 228, 232, 233, 234, 236, 237, 239, 240, 241], "summary": {"covered_lines": 78, "num_statements": 82, "percent_covered": 93.13725490196079, "percent_covered_display": "93.14", "missing_lines": 4, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [52, 57, 208, 229], "excluded_lines": [], "executed_branches": [[41, 42], [41, 44], [44, 46], [44, 49], [49, 60], [93, -87], [93, 95], [105, 106], [105, 109], [110, 111], [110, 121], [152, 153], [152, 157], [174, 175], [174, 186], [207, 211], [228, 232]], "missing_branches": [[49, 52], [207, 208], [228, 229]]}, "PRSelectionResult": {"executed_lines": [254, 255, 260, 265], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 18, 29, 62, 87, 131, 204, 221, 244, 245, 247, 257, 258, 262, 263, 268, 274], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/reply_service.py": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 13, 16, 17, 19, 22, 23, 24, 26, 27, 28, 29, 32, 33, 35, 41, 43, 78, 79, 80, 81, 83, 84, 86, 88, 89, 92, 98, 105, 106, 110, 111, 112, 113, 114, 122, 123, 127, 131, 132, 133, 135, 136, 139, 141, 154, 155, 156, 159, 160, 162, 165, 166, 168, 188, 199, 205, 206, 207, 208, 210, 212, 229, 231, 232, 243, 244, 247, 258, 259, 262, 263, 266, 267, 269, 271, 272, 273, 275, 276, 288, 348, 357, 358, 359, 364, 365, 366, 368, 370, 384, 386, 387, 389, 390, 393, 394, 396, 397, 399, 412, 414, 415, 417, 418, 420, 423, 424, 425, 426, 429, 430, 431, 432, 435, 436, 439, 441, 442, 447, 448, 450, 456, 469, 470, 471, 473, 474, 476], "summary": {"covered_lines": 130, "num_statements": 162, "percent_covered": 73.66071428571429, "percent_covered_display": "73.66", "missing_lines": 32, "excluded_lines": 0, "num_branches": 62, "num_partial_branches": 11, "covered_branches": 35, "missing_branches": 27}, "missing_lines": [117, 118, 200, 201, 202, 280, 281, 286, 307, 308, 320, 321, 323, 324, 327, 328, 329, 333, 334, 335, 337, 338, 339, 341, 343, 344, 345, 346, 452, 454, 482, 483], "excluded_lines": [], "executed_branches": [[78, 79], [78, 83], [89, 92], [89, 98], [105, 106], [105, 110], [111, 112], [113, 114], [122, 123], [122, 127], [135, 136], [135, 139], [155, 156], [155, 165], [159, 160], [159, 162], [199, 205], [205, 206], [205, 210], [207, 208], [258, 259], [258, 262], [262, 263], [262, 269], [266, 267], [275, 276], [358, 359], [358, 364], [365, 366], [365, 368], [423, 424], [430, 431], [435, 436], [439, 441], [447, 448]], "missing_branches": [[111, 122], [113, 117], [117, 118], [117, 122], [199, 200], [201, 202], [201, 205], [207, 210], [266, 269], [275, 280], [280, 281], [280, 286], [323, 324], [323, 333], [327, 328], [327, 329], [334, 335], [334, 337], [338, 339], [338, 341], [344, 345], [344, 346], [423, 435], [430, 435], [435, 439], [439, 450], [447, 450]], "functions": {"ReplyService.__init__": {"executed_lines": [41], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyService.post_reply": {"executed_lines": [78, 79, 80, 81, 83, 84, 86, 88, 89, 92, 98, 105, 106, 110, 111, 112, 113, 114, 122, 123, 127, 131, 132, 133, 135, 136, 139], "summary": {"covered_lines": 27, "num_statements": 29, "percent_covered": 86.66666666666667, "percent_covered_display": "86.67", "missing_lines": 2, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 2, "covered_branches": 12, "missing_branches": 4}, "missing_lines": [117, 118], "excluded_lines": [], "executed_branches": [[78, 79], [78, 83], [89, 92], [89, 98], [105, 106], [105, 110], [111, 112], [113, 114], [122, 123], [122, 127], [135, 136], [135, 139]], "missing_branches": [[111, 122], [113, 117], [117, 118], [117, 122]]}, "ReplyService._handle_graphql_errors": {"executed_lines": [154, 155, 156, 159, 160, 162, 165, 166], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[155, 156], [155, 165], [159, 160], [159, 162]], "missing_branches": []}, "ReplyService._build_reply_info_from_graphql": {"executed_lines": [188, 199, 205, 206, 207, 208, 210], "summary": {"covered_lines": 7, "num_statements": 10, "percent_covered": 61.111111111111114, "percent_covered_display": "61.11", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 4}, "missing_lines": [200, 201, 202], "excluded_lines": [], "executed_branches": [[199, 205], [205, 206], [205, 210], [207, 208]], "missing_branches": [[199, 200], [201, 202], [201, 205], [207, 210]]}, "ReplyService._post_reply_fallback_rest": {"executed_lines": [229, 231, 232, 243, 244, 247, 258, 259, 262, 263, 266, 267, 269, 271, 272, 273, 275, 276], "summary": {"covered_lines": 18, "num_statements": 21, "percent_covered": 77.41935483870968, "percent_covered_display": "77.42", "missing_lines": 3, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 4}, "missing_lines": [280, 281, 286], "excluded_lines": [], "executed_branches": [[258, 259], [258, 262], [262, 263], [262, 269], [266, 267], [275, 276]], "missing_branches": [[266, 269], [275, 280], [280, 281], [280, 286]]}, "ReplyService._get_review_id_for_comment": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 20, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 20, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 10}, "missing_lines": [307, 308, 320, 321, 323, 324, 327, 328, 329, 333, 334, 335, 337, 338, 339, 341, 343, 344, 345, 346], "excluded_lines": [], "executed_branches": [], "missing_branches": [[323, 324], [323, 333], [327, 328], [327, 329], [334, 335], [334, 337], [338, 339], [338, 341], [344, 345], [344, 346]]}, "ReplyService._get_repository_info": {"executed_lines": [357, 358, 359, 364, 365, 366, 368], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[358, 359], [358, 364], [365, 366], [365, 368]], "missing_branches": []}, "ReplyService.validate_comment_exists": {"executed_lines": [384, 386, 387, 389, 390, 393, 394, 396, 397], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyService._get_parent_comment_info": {"executed_lines": [412, 414, 415, 417, 418, 420, 423, 424, 425, 426, 429, 430, 431, 432, 435, 436, 439, 441, 442, 447, 448, 450], "summary": {"covered_lines": 22, "num_statements": 24, "percent_covered": 79.41176470588235, "percent_covered_display": "79.41", "missing_lines": 2, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 5, "covered_branches": 5, "missing_branches": 5}, "missing_lines": [452, 454], "excluded_lines": [], "executed_branches": [[423, 424], [430, 431], [435, 436], [439, 441], [447, 448]], "missing_branches": [[423, 435], [430, 435], [435, 439], [439, 450], [447, 450]]}, "ReplyService._get_pr_info": {"executed_lines": [469, 470, 471, 473, 474, 476], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [482, 483], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 13, 16, 17, 19, 22, 23, 24, 26, 27, 28, 29, 32, 33, 35, 43, 141, 168, 212, 288, 348, 370, 399, 456], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ReplyServiceError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "CommentNotFoundError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyService": {"executed_lines": [41, 78, 79, 80, 81, 83, 84, 86, 88, 89, 92, 98, 105, 106, 110, 111, 112, 113, 114, 122, 123, 127, 131, 132, 133, 135, 136, 139, 154, 155, 156, 159, 160, 162, 165, 166, 188, 199, 205, 206, 207, 208, 210, 229, 231, 232, 243, 244, 247, 258, 259, 262, 263, 266, 267, 269, 271, 272, 273, 275, 276, 357, 358, 359, 364, 365, 366, 368, 384, 386, 387, 389, 390, 393, 394, 396, 397, 412, 414, 415, 417, 418, 420, 423, 424, 425, 426, 429, 430, 431, 432, 435, 436, 439, 441, 442, 447, 448, 450, 469, 470, 471, 473, 474, 476], "summary": {"covered_lines": 105, "num_statements": 137, "percent_covered": 70.35175879396985, "percent_covered_display": "70.35", "missing_lines": 32, "excluded_lines": 0, "num_branches": 62, "num_partial_branches": 11, "covered_branches": 35, "missing_branches": 27}, "missing_lines": [117, 118, 200, 201, 202, 280, 281, 286, 307, 308, 320, 321, 323, 324, 327, 328, 329, 333, 334, 335, 337, 338, 339, 341, 343, 344, 345, 346, 452, 454, 482, 483], "excluded_lines": [], "executed_branches": [[78, 79], [78, 83], [89, 92], [89, 98], [105, 106], [105, 110], [111, 112], [113, 114], [122, 123], [122, 127], [135, 136], [135, 139], [155, 156], [155, 165], [159, 160], [159, 162], [199, 205], [205, 206], [205, 210], [207, 208], [258, 259], [258, 262], [262, 263], [262, 269], [266, 267], [275, 276], [358, 359], [358, 364], [365, 366], [365, 368], [423, 424], [430, 431], [435, 436], [439, 441], [447, 448]], "missing_branches": [[111, 122], [113, 117], [117, 118], [117, 122], [199, 200], [201, 202], [201, 205], [207, 210], [266, 269], [275, 280], [280, 281], [280, 286], [323, 324], [323, 333], [327, 328], [327, 329], [334, 335], [334, 337], [338, 339], [338, 341], [344, 345], [344, 346], [423, 435], [430, 435], [435, 439], [439, 450], [447, 450]]}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 13, 16, 17, 19, 22, 23, 24, 26, 27, 28, 29, 32, 33, 35, 43, 141, 168, 212, 288, 348, 370, 399, 456], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/resolve_service.py": {"executed_lines": [1, 3, 4, 6, 15, 16, 23, 24, 26, 32, 34, 50, 52, 53, 54, 55, 63, 64, 65, 68, 70, 71, 77, 78, 81, 82, 88, 89, 95, 97, 104, 120, 128, 136, 152, 154, 155, 165, 166, 167, 170, 172, 173, 179, 180, 183, 184, 190, 191, 197, 199, 206, 223, 231, 239, 254, 255, 264, 265, 266, 267, 274, 277, 279, 283, 287, 292, 302, 303, 305, 313, 316, 325, 327, 335, 356, 358, 360, 367, 374, 381, 391, 415, 422, 423, 424, 425, 433, 434, 435, 436, 437, 438, 442, 450, 459, 460, 462, 464, 467, 468, 475, 476, 482, 484, 485, 492, 493, 495, 496, 504, 506, 508, 509, 517, 530, 532, 556, 566, 571, 572, 573, 576, 577, 578, 579, 581, 582, 584, 590, 591, 597, 603, 612, 614, 615, 619, 624, 633, 635, 636, 637, 639, 643, 644, 647, 648], "summary": {"covered_lines": 145, "num_statements": 183, "percent_covered": 79.32489451476793, "percent_covered_display": "79.32", "missing_lines": 38, "excluded_lines": 0, "num_branches": 54, "num_partial_branches": 11, "covered_branches": 43, "missing_branches": 11}, "missing_lines": [69, 105, 129, 131, 156, 157, 171, 207, 232, 234, 256, 269, 272, 306, 308, 328, 330, 361, 368, 375, 382, 426, 427, 440, 451, 452, 456, 469, 473, 486, 490, 533, 534, 543, 599, 601, 620, 622], "excluded_lines": [], "executed_branches": [[77, 78], [77, 81], [88, 89], [88, 95], [179, 180], [179, 183], [190, 191], [190, 197], [255, 264], [265, 266], [265, 313], [267, 274], [279, 283], [279, 287], [287, 292], [287, 302], [360, 367], [367, 374], [374, 381], [381, 391], [433, 434], [433, 459], [436, 437], [436, 442], [437, 438], [462, 464], [462, 467], [468, 475], [475, 476], [475, 484], [485, 492], [495, 496], [495, 506], [572, 573], [572, 576], [577, 578], [581, 582], [584, 590], [584, 597], [614, 615], [614, 619], [636, 637], [636, 647]], "missing_branches": [[255, 256], [267, 269], [360, 361], [367, 368], [374, 375], [381, 382], [437, 440], [468, 469], [485, 486], [577, 597], [581, 597]], "functions": {"ResolveService.__init__": {"executed_lines": [32], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ResolveService.resolve_thread": {"executed_lines": [50, 52, 53, 54, 55, 63, 64, 65, 68, 70, 71, 77, 78, 81, 82, 88, 89, 95, 97, 104, 120, 128], "summary": {"covered_lines": 22, "num_statements": 26, "percent_covered": 86.66666666666667, "percent_covered_display": "86.67", "missing_lines": 4, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [69, 105, 129, 131], "excluded_lines": [], "executed_branches": [[77, 78], [77, 81], [88, 89], [88, 95]], "missing_branches": []}, "ResolveService.unresolve_thread": {"executed_lines": [152, 154, 155, 165, 166, 167, 170, 172, 173, 179, 180, 183, 184, 190, 191, 197, 199, 206, 223, 231], "summary": {"covered_lines": 20, "num_statements": 26, "percent_covered": 80.0, "percent_covered_display": "80.00", "missing_lines": 6, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [156, 157, 171, 207, 232, 234], "excluded_lines": [], "executed_branches": [[179, 180], [179, 183], [190, 191], [190, 197]], "missing_branches": []}, "ResolveService._handle_graphql_errors": {"executed_lines": [254, 255, 264, 265, 266, 267, 274, 277, 279, 283, 287, 292, 302, 303, 305, 313, 316, 325, 327], "summary": {"covered_lines": 19, "num_statements": 26, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 7, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2}, "missing_lines": [256, 269, 272, 306, 308, 328, 330], "excluded_lines": [], "executed_branches": [[255, 264], [265, 266], [265, 313], [267, 274], [279, 283], [279, 287], [287, 292], [287, 302]], "missing_branches": [[255, 256], [267, 269]]}, "ResolveService.validate_thread_exists": {"executed_lines": [356, 358, 360, 367, 374, 381, 391, 415, 422, 423, 424, 425, 433, 434, 435, 436, 437, 438, 442, 450, 459, 460, 462, 464, 467, 468, 475, 476, 482, 484, 485, 492, 493, 495, 496, 504, 506, 508, 509, 517, 530, 532], "summary": {"covered_lines": 42, "num_statements": 59, "percent_covered": 71.08433734939759, "percent_covered_display": "71.08", "missing_lines": 17, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 7, "covered_branches": 17, "missing_branches": 7}, "missing_lines": [361, 368, 375, 382, 426, 427, 440, 451, 452, 456, 469, 473, 486, 490, 533, 534, 543], "excluded_lines": [], "executed_branches": [[360, 367], [367, 374], [374, 381], [381, 391], [433, 434], [433, 459], [436, 437], [436, 442], [437, 438], [462, 464], [462, 467], [468, 475], [475, 476], [475, 484], [485, 492], [495, 496], [495, 506]], "missing_branches": [[360, 361], [367, 368], [374, 375], [381, 382], [437, 440], [468, 469], [485, 486]]}, "ResolveService._get_thread_url": {"executed_lines": [566, 571, 572, 573, 576, 577, 578, 579, 581, 582, 584, 590, 591, 597], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 2, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2}, "missing_lines": [599, 601], "excluded_lines": [], "executed_branches": [[572, 573], [572, 576], [577, 578], [581, 582], [584, 590], [584, 597]], "missing_branches": [[577, 597], [581, 597]]}, "ResolveService._extract_thread_url_fragment": {"executed_lines": [612, 614, 615, 619], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [620, 622], "excluded_lines": [], "executed_branches": [[614, 615], [614, 619]], "missing_branches": []}, "ResolveService._build_fallback_url": {"executed_lines": [633, 635, 636, 637, 639, 643, 644, 647, 648], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[636, 637], [636, 647]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 15, 16, 23, 24, 26, 34, 136, 239, 335, 556, 603, 624], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ResolveService": {"executed_lines": [32, 50, 52, 53, 54, 55, 63, 64, 65, 68, 70, 71, 77, 78, 81, 82, 88, 89, 95, 97, 104, 120, 128, 152, 154, 155, 165, 166, 167, 170, 172, 173, 179, 180, 183, 184, 190, 191, 197, 199, 206, 223, 231, 254, 255, 264, 265, 266, 267, 274, 277, 279, 283, 287, 292, 302, 303, 305, 313, 316, 325, 327, 356, 358, 360, 367, 374, 381, 391, 415, 422, 423, 424, 425, 433, 434, 435, 436, 437, 438, 442, 450, 459, 460, 462, 464, 467, 468, 475, 476, 482, 484, 485, 492, 493, 495, 496, 504, 506, 508, 509, 517, 530, 532, 566, 571, 572, 573, 576, 577, 578, 579, 581, 582, 584, 590, 591, 597, 612, 614, 615, 619, 633, 635, 636, 637, 639, 643, 644, 647, 648], "summary": {"covered_lines": 131, "num_statements": 169, "percent_covered": 78.02690582959642, "percent_covered_display": "78.03", "missing_lines": 38, "excluded_lines": 0, "num_branches": 54, "num_partial_branches": 11, "covered_branches": 43, "missing_branches": 11}, "missing_lines": [69, 105, 129, 131, 156, 157, 171, 207, 232, 234, 256, 269, 272, 306, 308, 328, 330, 361, 368, 375, 382, 426, 427, 440, 451, 452, 456, 469, 473, 486, 490, 533, 534, 543, 599, 601, 620, 622], "excluded_lines": [], "executed_branches": [[77, 78], [77, 81], [88, 89], [88, 95], [179, 180], [179, 183], [190, 191], [190, 197], [255, 264], [265, 266], [265, 313], [267, 274], [279, 283], [279, 287], [287, 292], [287, 302], [360, 367], [367, 374], [374, 381], [381, 391], [433, 434], [433, 459], [436, 437], [436, 442], [437, 438], [462, 464], [462, 467], [468, 475], [475, 476], [475, 484], [485, 492], [495, 496], [495, 506], [572, 573], [572, 576], [577, 578], [581, 582], [584, 590], [584, 597], [614, 615], [614, 619], [636, 637], [636, 647]], "missing_branches": [[255, 256], [267, 269], [360, 361], [367, 368], [374, 375], [381, 382], [437, 440], [468, 469], [485, 486], [577, 597], [581, 597]]}, "": {"executed_lines": [1, 3, 4, 6, 15, 16, 23, 24, 26, 34, 136, 239, 335, 556, 603, 624], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/utils.py": {"executed_lines": [1, 3, 4, 6, 9, 12, 24, 26, 27, 28, 35, 36, 43, 46, 47, 48, 49, 51, 52, 54, 55, 56, 66, 71, 72, 73, 74, 75, 76, 77, 80, 90, 92, 94, 95, 97, 105, 118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 137, 138, 140, 141, 143], "summary": {"covered_lines": 51, "num_statements": 53, "percent_covered": 96.1038961038961, "percent_covered_display": "96.10", "missing_lines": 2, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [57, 58], "excluded_lines": [], "executed_branches": [[27, 28], [27, 35], [35, 36], [35, 43], [47, 48], [47, 49], [49, 51], [49, 52], [52, 54], [52, 66], [55, 56], [72, 73], [72, 80], [94, 95], [94, 97], [118, 119], [118, 121], [121, 122], [121, 124], [124, 125], [124, 127], [127, 128], [127, 130]], "missing_branches": [[55, 66]], "functions": {"parse_datetime": {"executed_lines": [24, 26, 27, 28, 35, 36, 43, 46, 47, 48, 49, 51, 52, 54, 55, 56, 66, 71, 72, 73, 74, 75, 76, 77, 80, 90, 92, 94, 95, 97], "summary": {"covered_lines": 30, "num_statements": 32, "percent_covered": 93.75, "percent_covered_display": "93.75", "missing_lines": 2, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 1, "covered_branches": 15, "missing_branches": 1}, "missing_lines": [57, 58], "excluded_lines": [], "executed_branches": [[27, 28], [27, 35], [35, 36], [35, 43], [47, 48], [47, 49], [49, 51], [49, 52], [52, 54], [52, 66], [55, 56], [72, 73], [72, 80], [94, 95], [94, 97]], "missing_branches": [[55, 66]]}, "emit_error": {"executed_lines": [118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 137, 138, 140, 141, 143], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[118, 119], [118, 121], [121, 122], [121, 124], [124, 125], [124, 127], [127, 128], [127, 130]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 9, 12, 105], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 9, 12, 24, 26, 27, 28, 35, 36, 43, 46, 47, 48, 49, 51, 52, 54, 55, 56, 66, 71, 72, 73, 74, 75, 76, 77, 80, 90, 92, 94, 95, 97, 105, 118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 137, 138, 140, 141, 143], "summary": {"covered_lines": 51, "num_statements": 53, "percent_covered": 96.1038961038961, "percent_covered_display": "96.10", "missing_lines": 2, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [57, 58], "excluded_lines": [], "executed_branches": [[27, 28], [27, 35], [35, 36], [35, 43], [47, 48], [47, 49], [49, 51], [49, 52], [52, 54], [52, 66], [55, 56], [72, 73], [72, 80], [94, 95], [94, 97], [118, 119], [118, 121], [121, 122], [121, 124], [124, 125], [124, 127], [127, 128], [127, 130]], "missing_branches": [[55, 66]]}}}, "src/toady/validators/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/validators/node_id_validation.py": {"executed_lines": [1, 8, 9, 10, 13, 14, 17, 18, 21, 22, 23, 26, 29, 30, 33, 34, 35, 38, 41, 44, 45, 48, 51, 52, 55, 61, 67, 69, 75, 78, 80, 86, 88, 97, 98, 99, 100, 102, 112, 113, 115, 116, 120, 121, 122, 124, 139, 140, 143, 144, 145, 146, 149, 150, 151, 157, 158, 160, 161, 166, 167, 173, 174, 179, 181, 196, 197, 199, 200, 201, 204, 205, 206, 209, 211, 220, 222, 224, 225, 226, 227, 229, 234, 238, 240, 243, 245, 248, 250, 253, 255, 259, 271, 272, 275, 287, 288, 291, 293, 294, 297, 299, 300], "summary": {"covered_lines": 100, "num_statements": 100, "percent_covered": 99.24242424242425, "percent_covered_display": "99.24", "missing_lines": 0, "excluded_lines": 0, "num_branches": 32, "num_partial_branches": 1, "covered_branches": 31, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[97, 98], [97, 100], [98, 97], [98, 99], [112, 113], [112, 115], [115, 116], [115, 120], [121, -102], [121, 122], [139, 140], [139, 143], [144, 145], [144, 149], [149, 150], [149, 157], [160, 161], [160, 166], [166, 167], [166, 173], [173, 174], [173, 179], [196, 197], [196, 199], [200, 201], [200, 204], [204, 205], [204, 209], [224, 225], [226, 227], [226, 229]], "missing_branches": [[224, 234]], "functions": {"NodeIDValidator.__init__": {"executed_lines": [75, 78], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "NodeIDValidator.get_allowed_prefixes": {"executed_lines": [86], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "NodeIDValidator.identify_entity_type": {"executed_lines": [97, 98, 99, 100], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[97, 98], [97, 100], [98, 97], [98, 99]], "missing_branches": []}, "NodeIDValidator.validate_numeric_id": {"executed_lines": [112, 113, 115, 116, 120, 121, 122], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [115, 116], [115, 120], [121, -102], [121, 122]], "missing_branches": []}, "NodeIDValidator.validate_node_id_format": {"executed_lines": [139, 140, 143, 144, 145, 146, 149, 150, 151, 157, 158, 160, 161, 166, 167, 173, 174, 179], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[139, 140], [139, 143], [144, 145], [144, 149], [149, 150], [149, 157], [160, 161], [160, 166], [166, 167], [166, 173], [173, 174], [173, 179]], "missing_branches": []}, "NodeIDValidator.validate_id": {"executed_lines": [196, 197, 199, 200, 201, 204, 205, 206, 209], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[196, 197], [196, 199], [200, 201], [200, 204], [204, 205], [204, 209]], "missing_branches": []}, "NodeIDValidator.format_allowed_types_message": {"executed_lines": [220, 222, 224, 225, 226, 227, 229, 234], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 91.66666666666667, "percent_covered_display": "91.67", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[224, 225], [226, 227], [226, 229]], "missing_branches": [[224, 234]]}, "create_comment_validator": {"executed_lines": [240], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_thread_validator": {"executed_lines": [245], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_review_validator": {"executed_lines": [250], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_universal_validator": {"executed_lines": [255], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_comment_id": {"executed_lines": [271, 272], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_thread_id": {"executed_lines": [287, 288], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "get_comment_id_format_message": {"executed_lines": [293, 294], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "get_thread_id_format_message": {"executed_lines": [299, 300], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 10, 13, 14, 17, 18, 21, 22, 23, 26, 29, 30, 33, 34, 35, 38, 41, 44, 45, 48, 51, 52, 55, 61, 67, 69, 80, 88, 102, 124, 181, 211, 238, 243, 248, 253, 259, 275, 291, 297], "summary": {"covered_lines": 39, "num_statements": 39, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GitHubEntityType": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "NodeIDValidator": {"executed_lines": [75, 78, 86, 97, 98, 99, 100, 112, 113, 115, 116, 120, 121, 122, 139, 140, 143, 144, 145, 146, 149, 150, 151, 157, 158, 160, 161, 166, 167, 173, 174, 179, 196, 197, 199, 200, 201, 204, 205, 206, 209, 220, 222, 224, 225, 226, 227, 229, 234], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 98.76543209876543, "percent_covered_display": "98.77", "missing_lines": 0, "excluded_lines": 0, "num_branches": 32, "num_partial_branches": 1, "covered_branches": 31, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[97, 98], [97, 100], [98, 97], [98, 99], [112, 113], [112, 115], [115, 116], [115, 120], [121, -102], [121, 122], [139, 140], [139, 143], [144, 145], [144, 149], [149, 150], [149, 157], [160, 161], [160, 166], [166, 167], [166, 173], [173, 174], [173, 179], [196, 197], [196, 199], [200, 201], [200, 204], [204, 205], [204, 209], [224, 225], [226, 227], [226, 229]], "missing_branches": [[224, 234]]}, "": {"executed_lines": [1, 8, 9, 10, 13, 14, 17, 18, 21, 22, 23, 26, 29, 30, 33, 34, 35, 38, 41, 44, 45, 48, 51, 52, 55, 61, 67, 69, 80, 88, 102, 124, 181, 211, 238, 240, 243, 245, 248, 250, 253, 255, 259, 271, 272, 275, 287, 288, 291, 293, 294, 297, 299, 300], "summary": {"covered_lines": 51, "num_statements": 51, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/validators/schema_validator.py": {"executed_lines": [1, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 22, 23, 25, 38, 39, 40, 43, 44, 48, 65, 76, 77, 78, 79, 80, 82, 84, 85, 87, 89, 91, 93, 94, 95, 97, 98, 99, 101, 102, 106, 108, 109, 111, 112, 115, 116, 117, 118, 119, 120, 121, 123, 125, 126, 129, 130, 133, 139, 140, 142, 155, 156, 157, 158, 159, 160, 161, 163, 165, 167, 172, 174, 175, 177, 178, 179, 180, 181, 183, 184, 188, 190, 192, 193, 195, 197, 200, 201, 202, 203, 205, 214, 217, 219, 228, 229, 231, 234, 235, 241, 244, 245, 246, 247, 248, 254, 257, 258, 259, 269, 273, 275, 290, 294, 295, 298, 300, 302, 316, 318, 320, 321, 332, 335, 336, 337, 338, 348, 353, 355, 356, 357, 358, 359, 366, 381, 384, 385, 386, 387, 397, 398, 402, 403, 412, 421, 422, 425, 426, 428, 430, 439, 441, 463, 469, 473, 474, 476, 486, 487, 488, 490, 493, 499, 501, 507, 514, 517, 518, 522, 523, 527, 528, 529, 531, 532, 533, 535, 537, 543, 545, 548, 551, 552, 553, 554, 556, 558, 564, 574, 575, 576, 577, 578, 579, 580, 581, 583, 584, 588], "summary": {"covered_lines": 198, "num_statements": 224, "percent_covered": 83.98692810457516, "percent_covered_display": "83.99", "missing_lines": 26, "excluded_lines": 0, "num_branches": 82, "num_partial_branches": 19, "covered_branches": 59, "missing_branches": 23}, "missing_lines": [103, 104, 113, 186, 189, 191, 198, 215, 260, 266, 291, 296, 305, 306, 307, 308, 314, 450, 451, 453, 458, 459, 461, 470, 519, 524], "excluded_lines": [], "executed_branches": [[94, 95], [94, 97], [108, 109], [108, 111], [112, 115], [155, 156], [155, 163], [157, 158], [157, 163], [174, 175], [174, 177], [178, 179], [197, 200], [201, -195], [201, 202], [202, 203], [214, 217], [228, 229], [228, 231], [234, 235], [234, 244], [259, 269], [290, 294], [295, 298], [300, -275], [300, 302], [302, 316], [318, 320], [318, 335], [335, 336], [335, 348], [353, 300], [353, 355], [356, 357], [358, 300], [358, 359], [384, 385], [384, 397], [385, 384], [385, 386], [397, -366], [397, 398], [398, 397], [398, 402], [421, 422], [421, 425], [425, 426], [425, 428], [469, 473], [487, 488], [487, 490], [518, 522], [523, 527], [528, 529], [532, 533], [553, 554], [577, 578], [579, 580], [583, 584]], "missing_branches": [[112, 113], [178, 186], [197, 198], [202, 201], [214, 215], [259, 260], [290, 291], [295, 296], [302, 305], [307, 308], [307, 314], [356, 300], [450, 451], [450, 453], [469, 470], [518, 519], [523, 524], [528, 531], [532, 535], [553, 556], [577, 579], [579, 583], [583, 588]], "functions": {"SchemaValidationError.__init__": {"executed_lines": [38, 39, 40], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator.__init__": {"executed_lines": [76, 77, 78, 79, 80], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator._get_cache_path": {"executed_lines": [84, 85], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator._get_cache_metadata_path": {"executed_lines": [89], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator._is_cache_valid": {"executed_lines": [93, 94, 95, 97, 98, 99, 101, 102], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 2, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [103, 104], "excluded_lines": [], "executed_branches": [[94, 95], [94, 97]], "missing_branches": []}, "GitHubSchemaValidator._load_cached_schema": {"executed_lines": [108, 109, 111, 112, 115, 116, 117, 118, 119, 120, 121], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 87.5, "percent_covered_display": "87.50", "missing_lines": 1, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [113], "excluded_lines": [], "executed_branches": [[108, 109], [108, 111], [112, 115]], "missing_branches": [[112, 113]]}, "GitHubSchemaValidator._save_schema_to_cache": {"executed_lines": [125, 126, 129, 130, 133, 139, 140], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator.fetch_schema": {"executed_lines": [155, 156, 157, 158, 159, 160, 161, 163, 165, 167, 172, 174, 175, 177, 178, 179, 180, 181, 183, 184, 188, 190, 192, 193], "summary": {"covered_lines": 24, "num_statements": 27, "percent_covered": 88.57142857142857, "percent_covered_display": "88.57", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [186, 189, 191], "excluded_lines": [], "executed_branches": [[155, 156], [155, 163], [157, 158], [157, 163], [174, 175], [174, 177], [178, 179]], "missing_branches": [[178, 186]]}, "GitHubSchemaValidator._build_type_map": {"executed_lines": [197, 200, 201, 202, 203], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2}, "missing_lines": [198], "excluded_lines": [], "executed_branches": [[197, 200], [201, -195], [201, 202], [202, 203]], "missing_branches": [[197, 198], [202, 201]]}, "GitHubSchemaValidator.get_type": {"executed_lines": [214, 217], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 60.0, "percent_covered_display": "60.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [215], "excluded_lines": [], "executed_branches": [[214, 217]], "missing_branches": [[214, 215]]}, "GitHubSchemaValidator.validate_query": {"executed_lines": [228, 229, 231, 234, 235, 241, 244, 245, 246, 247, 248, 254, 257, 258, 259, 269, 273], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 88.0, "percent_covered_display": "88.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [260, 266], "excluded_lines": [], "executed_branches": [[228, 229], [228, 231], [234, 235], [234, 244], [259, 269]], "missing_branches": [[259, 260]]}, "GitHubSchemaValidator._validate_selections": {"executed_lines": [290, 294, 295, 298, 300, 302, 316, 318, 320, 321, 332, 335, 336, 337, 338, 348, 353, 355, 356, 357, 358, 359], "summary": {"covered_lines": 22, "num_statements": 29, "percent_covered": 73.46938775510205, "percent_covered_display": "73.47", "missing_lines": 7, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 4, "covered_branches": 14, "missing_branches": 6}, "missing_lines": [291, 296, 305, 306, 307, 308, 314], "excluded_lines": [], "executed_branches": [[290, 294], [295, 298], [300, -275], [300, 302], [302, 316], [318, 320], [318, 335], [335, 336], [335, 348], [353, 300], [353, 355], [356, 357], [358, 300], [358, 359]], "missing_branches": [[290, 291], [295, 296], [302, 305], [307, 308], [307, 314], [356, 300]]}, "GitHubSchemaValidator._validate_arguments": {"executed_lines": [381, 384, 385, 386, 387, 397, 398, 402, 403], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[384, 385], [384, 397], [385, 384], [385, 386], [397, -366], [397, 398], [398, 397], [398, 402]], "missing_branches": []}, "GitHubSchemaValidator._resolve_field_type": {"executed_lines": [421, 422, 425, 426, 428], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[421, 422], [421, 425], [425, 426], [425, 428]], "missing_branches": []}, "GitHubSchemaValidator._is_required_type": {"executed_lines": [439], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator.check_deprecations": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 6, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [450, 451, 453, 458, 459, 461], "excluded_lines": [], "executed_branches": [], "missing_branches": [[450, 451], [450, 453]]}, "GitHubSchemaValidator.get_schema_version": {"executed_lines": [469, 473, 474], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 66.66666666666667, "percent_covered_display": "66.67", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [470], "excluded_lines": [], "executed_branches": [[469, 473]], "missing_branches": [[469, 470]]}, "GitHubSchemaValidator.get_field_suggestions": {"executed_lines": [486, 487, 488, 490, 493, 499], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[487, 488], [487, 490]], "missing_branches": []}, "GitHubSchemaValidator.validate_mutations": {"executed_lines": [507, 514, 517, 518, 522, 523, 527, 528, 529, 531, 532, 533, 535], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 73.91304347826087, "percent_covered_display": "73.91", "missing_lines": 2, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 4, "covered_branches": 4, "missing_branches": 4}, "missing_lines": [519, 524], "excluded_lines": [], "executed_branches": [[518, 522], [523, 527], [528, 529], [532, 533]], "missing_branches": [[518, 519], [523, 524], [528, 531], [532, 535]]}, "GitHubSchemaValidator.validate_queries": {"executed_lines": [543, 545, 548, 551, 552, 553, 554, 556], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 90.0, "percent_covered_display": "90.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[553, 554]], "missing_branches": [[553, 556]]}, "GitHubSchemaValidator.generate_compatibility_report": {"executed_lines": [564, 574, 575, 576, 577, 578, 579, 580, 581, 583, 584, 588], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 3, "covered_branches": 3, "missing_branches": 3}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[577, 578], [579, 580], [583, 584]], "missing_branches": [[577, 579], [579, 583], [583, 588]]}, "": {"executed_lines": [1, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 22, 23, 25, 43, 44, 48, 65, 82, 87, 91, 106, 123, 142, 195, 205, 219, 275, 366, 412, 430, 441, 463, 476, 501, 537, 558], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"SchemaValidationError": {"executed_lines": [38, 39, 40], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator": {"executed_lines": [76, 77, 78, 79, 80, 84, 85, 89, 93, 94, 95, 97, 98, 99, 101, 102, 108, 109, 111, 112, 115, 116, 117, 118, 119, 120, 121, 125, 126, 129, 130, 133, 139, 140, 155, 156, 157, 158, 159, 160, 161, 163, 165, 167, 172, 174, 175, 177, 178, 179, 180, 181, 183, 184, 188, 190, 192, 193, 197, 200, 201, 202, 203, 214, 217, 228, 229, 231, 234, 235, 241, 244, 245, 246, 247, 248, 254, 257, 258, 259, 269, 273, 290, 294, 295, 298, 300, 302, 316, 318, 320, 321, 332, 335, 336, 337, 338, 348, 353, 355, 356, 357, 358, 359, 381, 384, 385, 386, 387, 397, 398, 402, 403, 421, 422, 425, 426, 428, 439, 469, 473, 474, 486, 487, 488, 490, 493, 499, 507, 514, 517, 518, 522, 523, 527, 528, 529, 531, 532, 533, 535, 543, 545, 548, 551, 552, 553, 554, 556, 564, 574, 575, 576, 577, 578, 579, 580, 581, 583, 584, 588], "summary": {"covered_lines": 161, "num_statements": 187, "percent_covered": 81.78438661710037, "percent_covered_display": "81.78", "missing_lines": 26, "excluded_lines": 0, "num_branches": 82, "num_partial_branches": 19, "covered_branches": 59, "missing_branches": 23}, "missing_lines": [103, 104, 113, 186, 189, 191, 198, 215, 260, 266, 291, 296, 305, 306, 307, 308, 314, 450, 451, 453, 458, 459, 461, 470, 519, 524], "excluded_lines": [], "executed_branches": [[94, 95], [94, 97], [108, 109], [108, 111], [112, 115], [155, 156], [155, 163], [157, 158], [157, 163], [174, 175], [174, 177], [178, 179], [197, 200], [201, -195], [201, 202], [202, 203], [214, 217], [228, 229], [228, 231], [234, 235], [234, 244], [259, 269], [290, 294], [295, 298], [300, -275], [300, 302], [302, 316], [318, 320], [318, 335], [335, 336], [335, 348], [353, 300], [353, 355], [356, 357], [358, 300], [358, 359], [384, 385], [384, 397], [385, 384], [385, 386], [397, -366], [397, 398], [398, 397], [398, 402], [421, 422], [421, 425], [425, 426], [425, 428], [469, 473], [487, 488], [487, 490], [518, 522], [523, 527], [528, 529], [532, 533], [553, 554], [577, 578], [579, 580], [583, 584]], "missing_branches": [[112, 113], [178, 186], [197, 198], [202, 201], [214, 215], [259, 260], [290, 291], [295, 296], [302, 305], [307, 308], [307, 314], [356, 300], [450, 451], [450, 453], [469, 470], [518, 519], [523, 524], [528, 531], [532, 535], [553, 556], [577, 579], [579, 583], [583, 588]]}, "": {"executed_lines": [1, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 22, 23, 25, 43, 44, 48, 65, 82, 87, 91, 106, 123, 142, 195, 205, 219, 275, 366, 412, 430, 441, 463, 476, 501, 537, 558], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/validators/validation.py": {"executed_lines": [1, 7, 8, 9, 10, 12, 13, 14, 21, 22, 23, 24, 25, 28, 29, 32, 35, 52, 70, 71, 72, 73, 81, 82, 83, 84, 91, 92, 99, 100, 110, 111, 118, 119, 126, 127, 137, 140, 158, 159, 167, 168, 169, 176, 177, 179, 180, 183, 184, 185, 186, 193, 196, 211, 212, 220, 221, 222, 229, 230, 231, 232, 233, 240, 243, 263, 264, 271, 272, 280, 283, 284, 292, 293, 300, 301, 312, 313, 314, 328, 329, 330, 340, 343, 363, 364, 372, 373, 374, 375, 382, 383, 390, 391, 401, 402, 409, 410, 417, 418, 425, 428, 441, 442, 449, 450, 457, 458, 459, 466, 467, 469, 470, 472, 474, 481, 489, 502, 503, 510, 511, 518, 519, 520, 527, 528, 535, 538, 551, 552, 559, 560, 567, 568, 569, 576, 577, 584, 587, 600, 601, 608, 609, 616, 617, 618, 625, 626, 635, 638, 658, 659, 666, 667, 674, 675, 676, 683, 684, 691, 692, 699, 702, 718, 719, 720, 721, 728, 729, 731, 732, 733, 734, 735, 736, 738, 746, 766, 767, 775, 776, 779, 780, 781, 782, 783, 785, 793, 813, 814, 821, 822, 830, 831, 832, 845, 846, 847, 848, 849, 862, 866, 875, 878, 879, 882, 883, 886, 887, 890, 891, 893, 897, 917, 925, 945, 953, 954, 955, 961, 962, 963, 964, 965, 968, 987, 988, 990, 991, 996, 997, 1003, 1004, 1009], "summary": {"covered_lines": 232, "num_statements": 236, "percent_covered": 98.6842105263158, "percent_covered_display": "98.68", "missing_lines": 4, "excluded_lines": 0, "num_branches": 144, "num_partial_branches": 1, "covered_branches": 143, "missing_branches": 1}, "missing_lines": [101, 102, 392, 393], "excluded_lines": [], "executed_branches": [[70, 71], [70, 81], [71, 72], [71, 73], [81, 82], [81, 110], [83, 84], [83, 91], [91, 92], [91, 99], [110, 111], [110, 118], [118, 119], [118, 126], [126, 127], [126, 137], [158, 159], [158, 167], [168, 169], [168, 176], [177, 179], [177, 183], [211, 212], [211, 220], [221, 222], [221, 229], [263, 264], [263, 271], [271, 272], [271, 280], [283, 284], [283, 292], [292, 293], [292, 300], [300, 301], [300, 312], [313, 314], [313, 328], [329, 330], [329, 340], [363, 364], [363, 372], [372, 373], [372, 401], [374, 375], [374, 382], [382, 383], [382, 390], [401, 402], [401, 409], [409, 410], [409, 417], [417, 418], [417, 425], [441, 442], [441, 449], [449, 450], [449, 457], [458, 459], [458, 466], [472, 474], [472, 481], [502, 503], [502, 510], [510, 511], [510, 518], [519, 520], [519, 527], [527, 528], [527, 535], [551, 552], [551, 559], [559, 560], [559, 567], [568, 569], [568, 576], [576, 577], [576, 584], [600, 601], [600, 608], [608, 609], [608, 616], [617, 618], [617, 625], [625, 626], [625, 635], [658, 659], [658, 666], [666, 667], [666, 674], [675, 676], [675, 683], [683, 684], [683, 691], [691, 692], [691, 699], [718, 719], [718, 728], [719, 720], [719, 721], [728, 729], [728, 731], [731, 732], [731, 738], [733, 734], [733, 735], [735, 736], [735, 738], [766, 767], [766, 775], [775, 776], [775, 779], [779, 780], [779, 785], [781, 782], [782, 781], [782, 783], [813, 814], [813, 821], [821, 822], [821, 830], [831, 832], [831, 845], [845, 846], [845, 862], [848, 849], [848, 862], [878, 879], [878, 882], [882, 883], [882, 886], [886, 887], [886, 890], [890, 891], [890, 893], [987, 988], [987, 990], [990, 991], [990, 996], [996, 997], [996, 1003], [1003, 1004], [1003, 1009]], "missing_branches": [[781, 785]], "functions": {"validate_pr_number": {"executed_lines": [70, 71, 72, 73, 81, 82, 83, 84, 91, 92, 99, 100, 110, 111, 118, 119, 126, 127, 137], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 94.5945945945946, "percent_covered_display": "94.59", "missing_lines": 2, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [101, 102], "excluded_lines": [], "executed_branches": [[70, 71], [70, 81], [71, 72], [71, 73], [81, 82], [81, 110], [83, 84], [83, 91], [91, 92], [91, 99], [110, 111], [110, 118], [118, 119], [118, 126], [126, 127], [126, 137]], "missing_branches": []}, "validate_comment_id": {"executed_lines": [158, 159, 167, 168, 169, 176, 177, 179, 180, 183, 184, 185, 186, 193], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[158, 159], [158, 167], [168, 169], [168, 176], [177, 179], [177, 183]], "missing_branches": []}, "validate_thread_id": {"executed_lines": [211, 212, 220, 221, 222, 229, 230, 231, 232, 233, 240], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[211, 212], [211, 220], [221, 222], [221, 229]], "missing_branches": []}, "validate_reply_body": {"executed_lines": [263, 264, 271, 272, 280, 283, 284, 292, 293, 300, 301, 312, 313, 314, 328, 329, 330, 340], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 0, "covered_branches": 14, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[263, 264], [263, 271], [271, 272], [271, 280], [283, 284], [283, 292], [292, 293], [292, 300], [300, 301], [300, 312], [313, 314], [313, 328], [329, 330], [329, 340]], "missing_branches": []}, "validate_limit": {"executed_lines": [363, 364, 372, 373, 374, 375, 382, 383, 390, 391, 401, 402, 409, 410, 417, 418, 425], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 93.93939393939394, "percent_covered_display": "93.94", "missing_lines": 2, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 0, "covered_branches": 14, "missing_branches": 0}, "missing_lines": [392, 393], "excluded_lines": [], "executed_branches": [[363, 364], [363, 372], [372, 373], [372, 401], [374, 375], [374, 382], [382, 383], [382, 390], [401, 402], [401, 409], [409, 410], [409, 417], [417, 418], [417, 425]], "missing_branches": []}, "validate_datetime_string": {"executed_lines": [441, 442, 449, 450, 457, 458, 459, 466, 467, 469, 470, 472, 474, 481], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[441, 442], [441, 449], [449, 450], [449, 457], [458, 459], [458, 466], [472, 474], [472, 481]], "missing_branches": []}, "validate_email": {"executed_lines": [502, 503, 510, 511, 518, 519, 520, 527, 528, 535], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[502, 503], [502, 510], [510, 511], [510, 518], [519, 520], [519, 527], [527, 528], [527, 535]], "missing_branches": []}, "validate_url": {"executed_lines": [551, 552, 559, 560, 567, 568, 569, 576, 577, 584], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[551, 552], [551, 559], [559, 560], [559, 567], [568, 569], [568, 576], [576, 577], [576, 584]], "missing_branches": []}, "validate_username": {"executed_lines": [600, 601, 608, 609, 616, 617, 618, 625, 626, 635], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[600, 601], [600, 608], [608, 609], [608, 616], [617, 618], [617, 625], [625, 626], [625, 635]], "missing_branches": []}, "validate_non_empty_string": {"executed_lines": [658, 659, 666, 667, 674, 675, 676, 683, 684, 691, 692, 699], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[658, 659], [658, 666], [666, 667], [666, 674], [675, 676], [675, 683], [683, 684], [683, 691], [691, 692], [691, 699]], "missing_branches": []}, "validate_boolean_flag": {"executed_lines": [718, 719, 720, 721, 728, 729, 731, 732, 733, 734, 735, 736, 738], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[718, 719], [718, 728], [719, 720], [719, 721], [728, 729], [728, 731], [731, 732], [731, 738], [733, 734], [733, 735], [735, 736], [735, 738]], "missing_branches": []}, "validate_choice": {"executed_lines": [766, 767, 775, 776, 779, 780, 781, 782, 783, 785], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 95.0, "percent_covered_display": "95.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[766, 767], [766, 775], [775, 776], [775, 779], [779, 780], [779, 785], [781, 782], [782, 781], [782, 783]], "missing_branches": [[781, 785]]}, "validate_dict_keys": {"executed_lines": [813, 814, 821, 822, 830, 831, 832, 845, 846, 847, 848, 849, 862], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[813, 814], [813, 821], [821, 822], [821, 830], [831, 832], [831, 845], [845, 846], [845, 862], [848, 849], [848, 862]], "missing_branches": []}, "validate_reply_content_warnings": {"executed_lines": [875, 878, 879, 882, 883, 886, 887, 890, 891, 893], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[878, 879], [878, 882], [882, 883], [882, 886], [886, 887], [886, 890], [890, 891], [890, 893]], "missing_branches": []}, "validate_fetch_command_args": {"executed_lines": [917], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_reply_command_args": {"executed_lines": [945], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_resolve_command_args": {"executed_lines": [987, 988, 990, 991, 996, 997, 1003, 1004, 1009], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[987, 988], [987, 990], [990, 991], [990, 996], [996, 997], [996, 1003], [1003, 1004], [1003, 1009]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 10, 12, 13, 14, 21, 22, 23, 24, 25, 28, 29, 32, 35, 52, 140, 196, 243, 343, 428, 489, 538, 587, 638, 702, 746, 793, 866, 897, 925, 953, 954, 955, 961, 962, 963, 964, 965, 968], "summary": {"covered_lines": 40, "num_statements": 40, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ResolveOptions": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 10, 12, 13, 14, 21, 22, 23, 24, 25, 28, 29, 32, 35, 52, 70, 71, 72, 73, 81, 82, 83, 84, 91, 92, 99, 100, 110, 111, 118, 119, 126, 127, 137, 140, 158, 159, 167, 168, 169, 176, 177, 179, 180, 183, 184, 185, 186, 193, 196, 211, 212, 220, 221, 222, 229, 230, 231, 232, 233, 240, 243, 263, 264, 271, 272, 280, 283, 284, 292, 293, 300, 301, 312, 313, 314, 328, 329, 330, 340, 343, 363, 364, 372, 373, 374, 375, 382, 383, 390, 391, 401, 402, 409, 410, 417, 418, 425, 428, 441, 442, 449, 450, 457, 458, 459, 466, 467, 469, 470, 472, 474, 481, 489, 502, 503, 510, 511, 518, 519, 520, 527, 528, 535, 538, 551, 552, 559, 560, 567, 568, 569, 576, 577, 584, 587, 600, 601, 608, 609, 616, 617, 618, 625, 626, 635, 638, 658, 659, 666, 667, 674, 675, 676, 683, 684, 691, 692, 699, 702, 718, 719, 720, 721, 728, 729, 731, 732, 733, 734, 735, 736, 738, 746, 766, 767, 775, 776, 779, 780, 781, 782, 783, 785, 793, 813, 814, 821, 822, 830, 831, 832, 845, 846, 847, 848, 849, 862, 866, 875, 878, 879, 882, 883, 886, 887, 890, 891, 893, 897, 917, 925, 945, 953, 954, 955, 961, 962, 963, 964, 965, 968, 987, 988, 990, 991, 996, 997, 1003, 1004, 1009], "summary": {"covered_lines": 232, "num_statements": 236, "percent_covered": 98.6842105263158, "percent_covered_display": "98.68", "missing_lines": 4, "excluded_lines": 0, "num_branches": 144, "num_partial_branches": 1, "covered_branches": 143, "missing_branches": 1}, "missing_lines": [101, 102, 392, 393], "excluded_lines": [], "executed_branches": [[70, 71], [70, 81], [71, 72], [71, 73], [81, 82], [81, 110], [83, 84], [83, 91], [91, 92], [91, 99], [110, 111], [110, 118], [118, 119], [118, 126], [126, 127], [126, 137], [158, 159], [158, 167], [168, 169], [168, 176], [177, 179], [177, 183], [211, 212], [211, 220], [221, 222], [221, 229], [263, 264], [263, 271], [271, 272], [271, 280], [283, 284], [283, 292], [292, 293], [292, 300], [300, 301], [300, 312], [313, 314], [313, 328], [329, 330], [329, 340], [363, 364], [363, 372], [372, 373], [372, 401], [374, 375], [374, 382], [382, 383], [382, 390], [401, 402], [401, 409], [409, 410], [409, 417], [417, 418], [417, 425], [441, 442], [441, 449], [449, 450], [449, 457], [458, 459], [458, 466], [472, 474], [472, 481], [502, 503], [502, 510], [510, 511], [510, 518], [519, 520], [519, 527], [527, 528], [527, 535], [551, 552], [551, 559], [559, 560], [559, 567], [568, 569], [568, 576], [576, 577], [576, 584], [600, 601], [600, 608], [608, 609], [608, 616], [617, 618], [617, 625], [625, 626], [625, 635], [658, 659], [658, 666], [666, 667], [666, 674], [675, 676], [675, 683], [683, 684], [683, 691], [691, 692], [691, 699], [718, 719], [718, 728], [719, 720], [719, 721], [728, 729], [728, 731], [731, 732], [731, 738], [733, 734], [733, 735], [735, 736], [735, 738], [766, 767], [766, 775], [775, 776], [775, 779], [779, 780], [779, 785], [781, 782], [782, 781], [782, 783], [813, 814], [813, 821], [821, 822], [821, 830], [831, 832], [831, 845], [845, 846], [845, 862], [848, 849], [848, 862], [878, 879], [878, 882], [882, 883], [882, 886], [886, 887], [886, 890], [890, 891], [890, 893], [987, 988], [987, 990], [990, 991], [990, 996], [996, 997], [996, 1003], [1003, 1004], [1003, 1009]], "missing_branches": [[781, 785]]}}}}, "totals": {"covered_lines": 3680, "num_statements": 4083, "percent_covered": 88.67082961641391, "percent_covered_display": "88.67", "missing_lines": 403, "excluded_lines": 68, "num_branches": 1522, "num_partial_branches": 146, "covered_branches": 1290, "missing_branches": 232}} diff --git a/coverage_unit.json b/coverage_unit.json new file mode 100644 index 0000000..c6fd2ed --- /dev/null +++ b/coverage_unit.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.8.2", "timestamp": "2025-06-12T22:27:04.057774", "branch_coverage": true, "show_contexts": false}, "files": {"src/toady/__init__.py": {"executed_lines": [1, 3, 4, 5], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1, 3, 4, 5], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 5], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/cli.py": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 22, 23, 88, 89, 93, 94, 95, 96, 99, 101, 102, 103, 105, 107, 108, 109, 111, 113, 114, 116, 119, 122], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 2, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [122, 123], "executed_branches": [[114, 116], [114, 119]], "missing_branches": [], "functions": {"cli": {"executed_lines": [88, 89], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "main": {"executed_lines": [101, 102, 103, 105, 107, 108, 109, 111, 113, 114, 116, 119], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[114, 116], [114, 119]], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 22, 23, 93, 94, 95, 96, 99, 122], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 2, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [122, 123], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 14, 15, 16, 22, 23, 88, 89, 93, 94, 95, 96, 99, 101, 102, 103, 105, 107, 108, 109, 111, 113, 114, 116, 119, 122], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 2, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [122, 123], "executed_branches": [[114, 116], [114, 119]], "missing_branches": []}}}, "src/toady/command_utils.py": {"executed_lines": [1, 3, 4, 6, 8, 9, 12, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 39, 41, 43, 45, 48, 57, 58, 61, 62, 63, 70, 80, 81, 82, 83], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[34, 35], [34, 39], [57, 58], [57, 61], [62, -48], [62, 63], [80, 81], [80, 82], [82, -70], [82, 83]], "missing_branches": [], "functions": {"handle_command_errors": {"executed_lines": [25, 26, 45], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "handle_command_errors.wrapper": {"executed_lines": [27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 39, 41, 43], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[34, 35], [34, 39]], "missing_branches": []}, "validate_pr_number": {"executed_lines": [57, 58, 61, 62, 63], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[57, 58], [57, 61], [62, -48], [62, 63]], "missing_branches": []}, "validate_limit": {"executed_lines": [80, 81, 82, 83], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[80, 81], [80, 82], [82, -70], [82, 83]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 8, 9, 12, 48, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 8, 9, 12, 25, 26, 27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 39, 41, 43, 45, 48, 57, 58, 61, 62, 63, 70, 80, 81, 82, 83], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[34, 35], [34, 39], [57, 58], [57, 61], [62, -48], [62, 63], [80, 81], [80, 82], [82, -70], [82, 83]], "missing_branches": []}}}, "src/toady/commands/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/commands/fetch.py": {"executed_lines": [1, 3, 5, 7, 11, 17, 20, 21, 30, 31, 32, 38, 46, 47, 122, 123, 124, 127, 128, 134, 137, 138, 140, 141, 150, 151, 154, 163, 165, 166, 168, 169, 176, 179, 180, 181, 182, 183, 186, 193, 194, 195, 196, 197, 200, 201, 202, 203, 204, 205, 207, 209], "summary": {"covered_lines": 51, "num_statements": 54, "percent_covered": 95.71428571428571, "percent_covered_display": "95.71", "missing_lines": 3, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [129, 130, 131], "excluded_lines": [], "executed_branches": [[122, 123], [122, 124], [150, 151], [150, 154], [179, 180], [179, 186], [194, 195], [194, 200], [195, 194], [195, 196], [200, 201], [200, 209], [202, 203], [202, 204], [204, 205], [204, 207]], "missing_branches": [], "functions": {"fetch": {"executed_lines": [122, 123, 124, 127, 128, 134, 137, 138, 140, 141, 150, 151, 154, 163, 165, 166, 168, 169, 176, 179, 180, 181, 182, 183, 186, 193, 194, 195, 196, 197, 200, 201, 202, 203, 204, 205, 207, 209], "summary": {"covered_lines": 38, "num_statements": 41, "percent_covered": 94.73684210526316, "percent_covered_display": "94.74", "missing_lines": 3, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [129, 130, 131], "excluded_lines": [], "executed_branches": [[122, 123], [122, 124], [150, 151], [150, 154], [179, 180], [179, 186], [194, 195], [194, 200], [195, 194], [195, 196], [200, 201], [200, 209], [202, 203], [202, 204], [204, 205], [204, 207]], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 11, 17, 20, 21, 30, 31, 32, 38, 46, 47], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 5, 7, 11, 17, 20, 21, 30, 31, 32, 38, 46, 47, 122, 123, 124, 127, 128, 134, 137, 138, 140, 141, 150, 151, 154, 163, 165, 166, 168, 169, 176, 179, 180, 181, 182, 183, 186, 193, 194, 195, 196, 197, 200, 201, 202, 203, 204, 205, 207, 209], "summary": {"covered_lines": 51, "num_statements": 54, "percent_covered": 95.71428571428571, "percent_covered_display": "95.71", "missing_lines": 3, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [129, 130, 131], "excluded_lines": [], "executed_branches": [[122, 123], [122, 124], [150, 151], [150, 154], [179, 180], [179, 186], [194, 195], [194, 200], [195, 194], [195, 196], [200, 201], [200, 209], [202, 203], [202, 204], [204, 205], [204, 207]], "missing_branches": []}}}, "src/toady/commands/reply.py": {"executed_lines": [1, 3, 4, 6, 8, 14, 20, 25, 28, 34, 91, 92, 95, 111, 112, 113, 115, 118, 119, 122, 123, 134, 136, 137, 140, 141, 153, 157, 160, 174, 177, 178, 179, 181, 182, 188, 189, 194, 195, 202, 203, 208, 211, 218, 221, 223, 224, 225, 226, 227, 228, 231, 232, 233, 234, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 250, 264, 275, 284, 285, 286, 289, 290, 292, 295, 302, 303, 306, 307, 313, 314, 320, 328, 329, 330, 333, 344, 385, 386, 387, 388, 389, 390, 391, 393, 400, 401, 404, 405, 406, 407, 408, 409, 412, 415, 419, 426, 428, 429, 430, 431, 432, 444, 447, 448, 449, 450, 451, 453, 460, 461, 464, 465, 475, 482, 483, 484, 491, 496, 497, 575, 576, 580, 581, 582, 583, 586, 587, 588, 589, 590, 593, 596, 599, 602, 603, 604, 606, 608, 609, 611, 612], "summary": {"covered_lines": 151, "num_statements": 156, "percent_covered": 94.39655172413794, "percent_covered_display": "94.40", "missing_lines": 5, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 8, "covered_branches": 68, "missing_branches": 8}, "missing_lines": [436, 443, 577, 614, 615], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [122, 123], [122, 134], [140, 141], [140, 153], [178, 179], [178, 181], [181, 182], [181, 188], [188, 189], [188, 194], [194, 195], [194, 202], [202, 203], [202, 208], [221, 223], [221, 227], [224, 225], [227, 228], [227, 231], [231, -211], [231, 232], [233, 234], [238, 239], [240, 241], [242, 243], [244, 245], [246, 247], [284, 285], [284, 289], [285, 284], [285, 286], [289, 290], [289, 292], [302, 303], [302, 306], [306, 307], [306, 313], [313, -295], [313, 314], [328, -320], [328, 329], [385, 386], [385, 404], [386, 385], [386, 387], [387, 388], [387, 393], [390, 391], [390, 401], [404, 405], [404, 447], [405, 406], [405, 428], [406, 407], [406, 419], [428, 429], [447, 448], [447, 453], [575, 576], [575, 580], [580, 581], [580, 582], [582, 583], [582, 586], [608, 609], [608, 611]], "missing_branches": [[224, 226], [233, 238], [238, 240], [240, 242], [242, 244], [244, 246], [246, -211], [428, 436]], "functions": {"_show_id_help": {"executed_lines": [34, 91, 92], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_reply_target_id": {"executed_lines": [111, 112, 113, 115, 118, 119, 122, 123, 134, 136, 137, 140, 141, 153, 157], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [122, 123], [122, 134], [140, 141], [140, 153]], "missing_branches": []}, "_validate_reply_args": {"executed_lines": [174, 177, 178, 179, 181, 182, 188, 189, 194, 195, 202, 203, 208], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[178, 179], [178, 181], [181, 182], [181, 188], [188, 189], [188, 194], [194, 195], [194, 202], [202, 203], [202, 208]], "missing_branches": []}, "_print_pretty_reply": {"executed_lines": [218, 221, 223, 224, 225, 226, 227, 228, 231, 232, 233, 234, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 7, "covered_branches": 13, "missing_branches": 7}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[221, 223], [221, 227], [224, 225], [227, 228], [227, 231], [231, -211], [231, 232], [233, 234], [238, 239], [240, 241], [242, 243], [244, 245], [246, 247]], "missing_branches": [[224, 226], [233, 238], [238, 240], [240, 242], [242, 244], [244, 246], [246, -211]]}, "_build_json_reply": {"executed_lines": [264, 275, 284, 285, 286, 289, 290, 292], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[284, 285], [284, 289], [285, 284], [285, 286], [289, 290], [289, 292]], "missing_branches": []}, "_show_warnings": {"executed_lines": [302, 303, 306, 307, 313, 314], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[302, 303], [302, 306], [306, 307], [306, 313], [313, -295], [313, 314]], "missing_branches": []}, "_show_progress": {"executed_lines": [328, 329, 330], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[328, -320], [328, 329]], "missing_branches": []}, "_handle_reply_error": {"executed_lines": [344, 385, 386, 387, 388, 389, 390, 391, 393, 400, 401, 404, 405, 406, 407, 408, 409, 412, 415, 419, 426, 428, 429, 430, 431, 432, 444, 447, 448, 449, 450, 451, 453, 460, 461], "summary": {"covered_lines": 35, "num_statements": 37, "percent_covered": 94.54545454545455, "percent_covered_display": "94.55", "missing_lines": 2, "excluded_lines": 0, "num_branches": 18, "num_partial_branches": 1, "covered_branches": 17, "missing_branches": 1}, "missing_lines": [436, 443], "excluded_lines": [], "executed_branches": [[385, 386], [385, 404], [386, 385], [386, 387], [387, 388], [387, 393], [390, 391], [390, 401], [404, 405], [404, 447], [405, 406], [405, 428], [406, 407], [406, 419], [428, 429], [447, 448], [447, 453]], "missing_branches": [[428, 436]]}, "reply": {"executed_lines": [575, 576, 580, 581, 582, 583, 586, 587, 588, 589, 590, 593, 596, 599, 602, 603, 604, 606, 608, 609, 611, 612], "summary": {"covered_lines": 22, "num_statements": 25, "percent_covered": 90.9090909090909, "percent_covered_display": "90.91", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [577, 614, 615], "excluded_lines": [], "executed_branches": [[575, 576], [575, 580], [580, 581], [580, 582], [582, 583], [582, 586], [608, 609], [608, 611]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 8, 14, 20, 25, 28, 95, 160, 211, 250, 295, 320, 333, 464, 465, 475, 482, 483, 484, 491, 496, 497], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 8, 14, 20, 25, 28, 34, 91, 92, 95, 111, 112, 113, 115, 118, 119, 122, 123, 134, 136, 137, 140, 141, 153, 157, 160, 174, 177, 178, 179, 181, 182, 188, 189, 194, 195, 202, 203, 208, 211, 218, 221, 223, 224, 225, 226, 227, 228, 231, 232, 233, 234, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 250, 264, 275, 284, 285, 286, 289, 290, 292, 295, 302, 303, 306, 307, 313, 314, 320, 328, 329, 330, 333, 344, 385, 386, 387, 388, 389, 390, 391, 393, 400, 401, 404, 405, 406, 407, 408, 409, 412, 415, 419, 426, 428, 429, 430, 431, 432, 444, 447, 448, 449, 450, 451, 453, 460, 461, 464, 465, 475, 482, 483, 484, 491, 496, 497, 575, 576, 580, 581, 582, 583, 586, 587, 588, 589, 590, 593, 596, 599, 602, 603, 604, 606, 608, 609, 611, 612], "summary": {"covered_lines": 151, "num_statements": 156, "percent_covered": 94.39655172413794, "percent_covered_display": "94.40", "missing_lines": 5, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 8, "covered_branches": 68, "missing_branches": 8}, "missing_lines": [436, 443, 577, 614, 615], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [122, 123], [122, 134], [140, 141], [140, 153], [178, 179], [178, 181], [181, 182], [181, 188], [188, 189], [188, 194], [194, 195], [194, 202], [202, 203], [202, 208], [221, 223], [221, 227], [224, 225], [227, 228], [227, 231], [231, -211], [231, 232], [233, 234], [238, 239], [240, 241], [242, 243], [244, 245], [246, 247], [284, 285], [284, 289], [285, 284], [285, 286], [289, 290], [289, 292], [302, 303], [302, 306], [306, 307], [306, 313], [313, -295], [313, 314], [328, -320], [328, 329], [385, 386], [385, 404], [386, 385], [386, 387], [387, 388], [387, 393], [390, 391], [390, 401], [404, 405], [404, 447], [405, 406], [405, 428], [406, 407], [406, 419], [428, 429], [447, 448], [447, 453], [575, 576], [575, 580], [580, 581], [580, 582], [582, 583], [582, 586], [608, 609], [608, 611]], "missing_branches": [[224, 226], [233, 238], [238, 240], [240, 242], [242, 244], [244, 246], [246, -211], [428, 436]]}}}, "src/toady/commands/resolve.py": {"executed_lines": [1, 3, 4, 5, 7, 9, 10, 18, 23, 24, 25, 28, 42, 43, 45, 47, 48, 55, 56, 58, 60, 63, 83, 84, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 101, 106, 109, 128, 129, 133, 134, 135, 136, 138, 139, 140, 145, 146, 147, 149, 150, 153, 154, 156, 157, 158, 159, 160, 161, 165, 166, 167, 168, 169, 172, 175, 197, 198, 199, 200, 201, 202, 203, 204, 205, 207, 216, 219, 228, 229, 231, 234, 245, 246, 247, 249, 258, 261, 277, 290, 291, 292, 293, 294, 295, 296, 298, 305, 306, 309, 310, 313, 314, 316, 323, 324, 327, 340, 342, 344, 347, 348, 349, 352, 357, 362, 374, 375, 377, 378, 381, 396, 397, 401, 402, 405, 406, 409, 410, 413, 415, 418, 430, 431, 432, 434, 435, 436, 437, 439, 442, 450, 451, 452, 453, 456, 466, 467, 468, 469, 470, 472, 475, 491, 493, 511, 512, 513, 514, 515, 516, 517, 519, 526, 527, 530, 531, 532, 534, 541, 542, 545, 557, 560, 563, 564, 566, 567, 569, 571, 573, 574, 577, 578, 585, 592, 600, 606, 612, 613, 614, 621, 622, 718, 719, 720, 721, 722, 726, 729, 732, 733, 734, 735, 738, 741], "summary": {"covered_lines": 205, "num_statements": 207, "percent_covered": 98.29351535836177, "percent_covered_display": "98.29", "missing_lines": 2, "excluded_lines": 0, "num_branches": 86, "num_partial_branches": 3, "covered_branches": 83, "missing_branches": 3}, "missing_lines": [170, 737], "excluded_lines": [], "executed_branches": [[42, 43], [42, 45], [55, 56], [55, 58], [83, 84], [83, 86], [86, 87], [86, 101], [91, 92], [91, 93], [93, 94], [93, 95], [96, -63], [96, 97], [128, 129], [128, 133], [138, 139], [138, 172], [139, 140], [139, 145], [146, 147], [146, 149], [153, 138], [153, 154], [159, 160], [169, 138], [197, 198], [197, 207], [201, -175], [201, 202], [204, -175], [204, 205], [228, 229], [228, 231], [245, 246], [245, 249], [290, 291], [290, 309], [291, 290], [291, 292], [292, 293], [292, 298], [295, 296], [295, 306], [309, 310], [309, 313], [313, 314], [313, 316], [347, 348], [347, 352], [374, -327], [374, 375], [396, 397], [396, 401], [401, 402], [401, 405], [405, 406], [405, 409], [409, 410], [409, 413], [431, 432], [431, 434], [450, -442], [450, 451], [466, 467], [466, 472], [469, -456], [469, 470], [511, 512], [511, 530], [512, 511], [512, 513], [513, 514], [513, 519], [516, 517], [516, 527], [530, 531], [531, 532], [531, 534], [566, 567], [566, 569], [732, 733], [732, 741]], "missing_branches": [[159, 165], [169, 170], [530, -475]], "functions": {"_fetch_and_filter_threads": {"executed_lines": [42, 43, 45, 47, 48, 55, 56, 58, 60], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[42, 43], [42, 45], [55, 56], [55, 58]], "missing_branches": []}, "_handle_confirmation_prompt": {"executed_lines": [83, 84, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 101, 106], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[83, 84], [83, 86], [86, 87], [86, 101], [91, 92], [91, 93], [93, 94], [93, 95], [96, -63], [96, 97]], "missing_branches": []}, "_process_threads": {"executed_lines": [128, 129, 133, 134, 135, 136, 138, 139, 140, 145, 146, 147, 149, 150, 153, 154, 156, 157, 158, 159, 160, 161, 165, 166, 167, 168, 169, 172], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 93.02325581395348, "percent_covered_display": "93.02", "missing_lines": 1, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 2, "covered_branches": 12, "missing_branches": 2}, "missing_lines": [170], "excluded_lines": [], "executed_branches": [[128, 129], [128, 133], [138, 139], [138, 172], [139, 140], [139, 145], [146, 147], [146, 149], [153, 138], [153, 154], [159, 160], [169, 138]], "missing_branches": [[159, 165], [169, 170]]}, "_display_summary": {"executed_lines": [197, 198, 199, 200, 201, 202, 203, 204, 205, 207, 216], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[197, 198], [197, 207], [201, -175], [201, 202], [204, -175], [204, 205]], "missing_branches": []}, "_get_action_labels": {"executed_lines": [228, 229, 231], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[228, 229], [228, 231]], "missing_branches": []}, "_handle_empty_threads": {"executed_lines": [245, 246, 247, 249, 258], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[245, 246], [245, 249]], "missing_branches": []}, "_handle_bulk_resolve_error": {"executed_lines": [277, 290, 291, 292, 293, 294, 295, 296, 298, 305, 306, 309, 310, 313, 314, 316, 323, 324], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[290, 291], [290, 309], [291, 290], [291, 292], [292, 293], [292, 298], [295, 296], [295, 306], [309, 310], [309, 313], [313, 314], [313, 316]], "missing_branches": []}, "_handle_bulk_resolve": {"executed_lines": [340, 342, 344, 347, 348, 349, 352, 357, 362, 374, 375, 377, 378], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[347, 348], [347, 352], [374, -327], [374, 375]], "missing_branches": []}, "_validate_resolve_parameters": {"executed_lines": [396, 397, 401, 402, 405, 406, 409, 410, 413, 415], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[396, 397], [396, 401], [401, 402], [401, 405], [405, 406], [405, 409], [409, 410], [409, 413]], "missing_branches": []}, "_validate_and_prepare_thread_id": {"executed_lines": [430, 431, 432, 434, 435, 436, 437, 439], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[431, 432], [431, 434]], "missing_branches": []}, "_show_single_resolve_progress": {"executed_lines": [450, 451, 452, 453], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[450, -442], [450, 451]], "missing_branches": []}, "_handle_single_resolve_success": {"executed_lines": [466, 467, 468, 469, 470, 472], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[466, 467], [466, 472], [469, -456], [469, 470]], "missing_branches": []}, "_handle_single_resolve_error": {"executed_lines": [491, 493, 511, 512, 513, 514, 515, 516, 517, 519, 526, 527, 530, 531, 532, 534, 541, 542], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.66666666666667, "percent_covered_display": "96.67", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 1, "covered_branches": 11, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[511, 512], [511, 530], [512, 511], [512, 513], [513, 514], [513, 519], [516, 517], [516, 527], [530, 531], [531, 532], [531, 534]], "missing_branches": [[530, -475]]}, "_handle_single_resolve": {"executed_lines": [557, 560, 563, 564, 566, 567, 569, 571, 573, 574], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[566, 567], [566, 569]], "missing_branches": []}, "resolve": {"executed_lines": [718, 719, 720, 721, 722, 726, 729, 732, 733, 734, 735, 738, 741], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 93.75, "percent_covered_display": "93.75", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [737], "excluded_lines": [], "executed_branches": [[732, 733], [732, 741]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 9, 10, 18, 23, 24, 25, 28, 63, 109, 175, 219, 234, 261, 327, 381, 418, 442, 456, 475, 545, 577, 578, 585, 592, 600, 606, 612, 613, 614, 621, 622], "summary": {"covered_lines": 35, "num_statements": 35, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 9, 10, 18, 23, 24, 25, 28, 42, 43, 45, 47, 48, 55, 56, 58, 60, 63, 83, 84, 86, 87, 91, 92, 93, 94, 95, 96, 97, 98, 101, 106, 109, 128, 129, 133, 134, 135, 136, 138, 139, 140, 145, 146, 147, 149, 150, 153, 154, 156, 157, 158, 159, 160, 161, 165, 166, 167, 168, 169, 172, 175, 197, 198, 199, 200, 201, 202, 203, 204, 205, 207, 216, 219, 228, 229, 231, 234, 245, 246, 247, 249, 258, 261, 277, 290, 291, 292, 293, 294, 295, 296, 298, 305, 306, 309, 310, 313, 314, 316, 323, 324, 327, 340, 342, 344, 347, 348, 349, 352, 357, 362, 374, 375, 377, 378, 381, 396, 397, 401, 402, 405, 406, 409, 410, 413, 415, 418, 430, 431, 432, 434, 435, 436, 437, 439, 442, 450, 451, 452, 453, 456, 466, 467, 468, 469, 470, 472, 475, 491, 493, 511, 512, 513, 514, 515, 516, 517, 519, 526, 527, 530, 531, 532, 534, 541, 542, 545, 557, 560, 563, 564, 566, 567, 569, 571, 573, 574, 577, 578, 585, 592, 600, 606, 612, 613, 614, 621, 622, 718, 719, 720, 721, 722, 726, 729, 732, 733, 734, 735, 738, 741], "summary": {"covered_lines": 205, "num_statements": 207, "percent_covered": 98.29351535836177, "percent_covered_display": "98.29", "missing_lines": 2, "excluded_lines": 0, "num_branches": 86, "num_partial_branches": 3, "covered_branches": 83, "missing_branches": 3}, "missing_lines": [170, 737], "excluded_lines": [], "executed_branches": [[42, 43], [42, 45], [55, 56], [55, 58], [83, 84], [83, 86], [86, 87], [86, 101], [91, 92], [91, 93], [93, 94], [93, 95], [96, -63], [96, 97], [128, 129], [128, 133], [138, 139], [138, 172], [139, 140], [139, 145], [146, 147], [146, 149], [153, 138], [153, 154], [159, 160], [169, 138], [197, 198], [197, 207], [201, -175], [201, 202], [204, -175], [204, 205], [228, 229], [228, 231], [245, 246], [245, 249], [290, 291], [290, 309], [291, 290], [291, 292], [292, 293], [292, 298], [295, 296], [295, 306], [309, 310], [309, 313], [313, 314], [313, 316], [347, 348], [347, 352], [374, -327], [374, 375], [396, 397], [396, 401], [401, 402], [401, 405], [405, 406], [405, 409], [409, 410], [409, 413], [431, 432], [431, 434], [450, -442], [450, 451], [466, 467], [466, 472], [469, -456], [469, 470], [511, 512], [511, 530], [512, 511], [512, 513], [513, 514], [513, 519], [516, 517], [516, 527], [530, 531], [531, 532], [531, 534], [566, 567], [566, 569], [732, 733], [732, 741]], "missing_branches": [[159, 165], [169, 170], [530, -475]]}}}, "src/toady/commands/schema.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 10, 17, 23, 24, 25, 27, 28, 31, 32, 37, 42, 48, 50, 52, 61, 62, 63, 64, 69, 70, 75, 76, 77, 78, 79, 83, 84, 91, 92, 93, 94, 95, 100, 101, 102, 104, 105, 106, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 124, 125, 126, 128, 129, 133, 134, 135, 136, 137, 138, 141, 142, 147, 152, 154, 156, 165, 166, 167, 168, 173, 174, 179, 180, 181, 182, 183, 187, 188, 195, 196, 197, 198, 199, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 214, 215, 219, 220, 221, 222, 223, 224, 227, 228, 229, 234, 240, 242, 244, 245, 252, 261, 262, 263, 264, 269, 270, 275, 276, 277, 278, 279, 285, 286, 287, 289, 290, 291, 296, 297, 298, 299, 304, 307, 309, 310, 314, 315, 316, 317, 322, 328, 329, 330, 335, 336, 337, 340, 341, 343, 344, 346, 347, 357, 358, 359, 360, 362, 363, 364, 366, 367, 368, 369, 370, 371, 376, 377, 378, 379, 381, 382, 383, 385, 386, 387, 388, 395, 396, 397, 398, 399, 401, 404, 410, 411, 412, 417, 418, 419, 422, 423, 425, 426, 428, 430, 431, 432, 435, 437, 438, 445, 446, 447, 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459, 462, 468, 469, 470, 476, 477, 478, 479, 480, 481, 485, 488, 489, 490, 491, 492, 493, 497, 499, 501, 502], "summary": {"covered_lines": 240, "num_statements": 261, "percent_covered": 89.21832884097034, "percent_covered_display": "89.22", "missing_lines": 21, "excluded_lines": 0, "num_branches": 110, "num_partial_branches": 11, "covered_branches": 91, "missing_branches": 19}, "missing_lines": [53, 130, 131, 132, 157, 216, 217, 218, 253, 300, 305, 306, 311, 312, 313, 318, 319, 373, 389, 390, 392], "excluded_lines": [], "executed_branches": [[27, -23], [27, 28], [52, 61], [101, 102], [101, 104], [121, 122], [121, 125], [123, 124], [123, 125], [129, 133], [133, 134], [133, 135], [156, 165], [203, 204], [215, 219], [219, 220], [219, 221], [244, 245], [244, 252], [252, 261], [286, 287], [286, 289], [310, 314], [314, 315], [314, 316], [329, 330], [329, 335], [359, 360], [359, 362], [362, 363], [362, 376], [366, 367], [366, 370], [368, 362], [368, 369], [370, 371], [378, 379], [378, 381], [381, 382], [381, 395], [385, 386], [387, 381], [387, 388], [396, 397], [396, 401], [398, 399], [398, 401], [411, 412], [411, 417], [417, 418], [417, 422], [425, 426], [425, 445], [426, 428], [426, 430], [431, 432], [431, 435], [445, 446], [445, 454], [447, 448], [447, 454], [449, 450], [449, 451], [451, 447], [451, 452], [454, -404], [454, 455], [456, -404], [456, 457], [458, 456], [458, 459], [469, 470], [469, 476], [477, 478], [477, 488], [478, 479], [478, 488], [479, 478], [479, 480], [480, 478], [480, 481], [481, 480], [481, 485], [489, 490], [490, 491], [490, 499], [491, 492], [492, 490], [492, 493], [493, 492], [493, 497]], "missing_branches": [[52, 53], [129, 130], [131, 132], [131, 133], [156, 157], [203, -141], [215, 216], [217, 218], [217, 219], [252, 253], [310, 311], [312, 313], [312, 314], [370, 373], [385, 389], [389, 390], [389, 392], [489, 499], [491, 490]], "functions": {"schema": {"executed_lines": [27, 28], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[27, -23], [27, 28]], "missing_branches": []}, "validate": {"executed_lines": [50, 52, 61, 62, 63, 64, 69, 70, 75, 76, 77, 78, 79, 83, 84, 91, 92, 93, 94, 95, 100, 101, 102, 104, 105, 106, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 124, 125, 126, 128, 129, 133, 134, 135, 136, 137, 138], "summary": {"covered_lines": 47, "num_statements": 51, "percent_covered": 87.6923076923077, "percent_covered_display": "87.69", "missing_lines": 4, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 4}, "missing_lines": [53, 130, 131, 132], "excluded_lines": [], "executed_branches": [[52, 61], [101, 102], [101, 104], [121, 122], [121, 125], [123, 124], [123, 125], [129, 133], [133, 134], [133, 135]], "missing_branches": [[52, 53], [129, 130], [131, 132], [131, 133]]}, "fetch": {"executed_lines": [154, 156, 165, 166, 167, 168, 173, 174, 179, 180, 181, 182, 183, 187, 188, 195, 196, 197, 198, 199, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 214, 215, 219, 220, 221, 222, 223, 224], "summary": {"covered_lines": 38, "num_statements": 42, "percent_covered": 82.6923076923077, "percent_covered_display": "82.69", "missing_lines": 4, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 3, "covered_branches": 5, "missing_branches": 5}, "missing_lines": [157, 216, 217, 218], "excluded_lines": [], "executed_branches": [[156, 165], [203, 204], [215, 219], [219, 220], [219, 221]], "missing_branches": [[156, 157], [203, -141], [215, 216], [217, 218], [217, 219]]}, "check": {"executed_lines": [242, 244, 245, 252, 261, 262, 263, 264, 269, 270, 275, 276, 277, 278, 279, 285, 286, 287, 289, 290, 291, 296, 297, 298, 299, 304, 307, 309, 310, 314, 315, 316, 317], "summary": {"covered_lines": 33, "num_statements": 42, "percent_covered": 75.92592592592592, "percent_covered_display": "75.93", "missing_lines": 9, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 4}, "missing_lines": [253, 300, 305, 306, 311, 312, 313, 318, 319], "excluded_lines": [], "executed_branches": [[244, 245], [244, 252], [252, 261], [286, 287], [286, 289], [310, 314], [314, 315], [314, 316]], "missing_branches": [[252, 253], [310, 311], [312, 313], [312, 314]]}, "_display_summary_report": {"executed_lines": [328, 329, 330, 335, 336, 337, 340, 341, 343, 344, 346, 347, 357, 358, 359, 360, 362, 363, 364, 366, 367, 368, 369, 370, 371, 376, 377, 378, 379, 381, 382, 383, 385, 386, 387, 388, 395, 396, 397, 398, 399, 401], "summary": {"covered_lines": 42, "num_statements": 46, "percent_covered": 88.88888888888889, "percent_covered_display": "88.89", "missing_lines": 4, "excluded_lines": 0, "num_branches": 26, "num_partial_branches": 2, "covered_branches": 22, "missing_branches": 4}, "missing_lines": [373, 389, 390, 392], "excluded_lines": [], "executed_branches": [[329, 330], [329, 335], [359, 360], [359, 362], [362, 363], [362, 376], [366, 367], [366, 370], [368, 362], [368, 369], [370, 371], [378, 379], [378, 381], [381, 382], [381, 395], [385, 386], [387, 381], [387, 388], [396, 397], [396, 401], [398, 399], [398, 401]], "missing_branches": [[370, 373], [385, 389], [389, 390], [389, 392]]}, "_display_query_validation_results": {"executed_lines": [410, 411, 412, 417, 418, 419, 422, 423, 425, 426, 428, 430, 431, 432, 435, 437, 438, 445, 446, 447, 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 0, "covered_branches": 24, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[411, 412], [411, 417], [417, 418], [417, 422], [425, 426], [425, 445], [426, 428], [426, 430], [431, 432], [431, 435], [445, 446], [445, 454], [447, 448], [447, 454], [449, 450], [449, 451], [451, 447], [451, 452], [454, -404], [454, 455], [456, -404], [456, 457], [458, 456], [458, 459]], "missing_branches": []}, "_has_critical_errors": {"executed_lines": [468, 469, 470, 476, 477, 478, 479, 480, 481, 485, 488, 489, 490, 491, 492, 493, 497, 499, 501, 502], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 95.23809523809524, "percent_covered_display": "95.24", "missing_lines": 0, "excluded_lines": 0, "num_branches": 22, "num_partial_branches": 2, "covered_branches": 20, "missing_branches": 2}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[469, 470], [469, 476], [477, 478], [477, 488], [478, 479], [478, 488], [479, 478], [479, 480], [480, 478], [480, 481], [481, 480], [481, 485], [489, 490], [490, 491], [490, 499], [491, 492], [492, 490], [492, 493], [493, 492], [493, 497]], "missing_branches": [[489, 499], [491, 490]]}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 10, 17, 23, 24, 25, 31, 32, 37, 42, 48, 141, 142, 147, 152, 227, 228, 229, 234, 240, 322, 404, 462], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 6, 8, 10, 17, 23, 24, 25, 27, 28, 31, 32, 37, 42, 48, 50, 52, 61, 62, 63, 64, 69, 70, 75, 76, 77, 78, 79, 83, 84, 91, 92, 93, 94, 95, 100, 101, 102, 104, 105, 106, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 124, 125, 126, 128, 129, 133, 134, 135, 136, 137, 138, 141, 142, 147, 152, 154, 156, 165, 166, 167, 168, 173, 174, 179, 180, 181, 182, 183, 187, 188, 195, 196, 197, 198, 199, 202, 203, 204, 205, 206, 207, 209, 210, 211, 212, 214, 215, 219, 220, 221, 222, 223, 224, 227, 228, 229, 234, 240, 242, 244, 245, 252, 261, 262, 263, 264, 269, 270, 275, 276, 277, 278, 279, 285, 286, 287, 289, 290, 291, 296, 297, 298, 299, 304, 307, 309, 310, 314, 315, 316, 317, 322, 328, 329, 330, 335, 336, 337, 340, 341, 343, 344, 346, 347, 357, 358, 359, 360, 362, 363, 364, 366, 367, 368, 369, 370, 371, 376, 377, 378, 379, 381, 382, 383, 385, 386, 387, 388, 395, 396, 397, 398, 399, 401, 404, 410, 411, 412, 417, 418, 419, 422, 423, 425, 426, 428, 430, 431, 432, 435, 437, 438, 445, 446, 447, 448, 449, 450, 451, 452, 454, 455, 456, 457, 458, 459, 462, 468, 469, 470, 476, 477, 478, 479, 480, 481, 485, 488, 489, 490, 491, 492, 493, 497, 499, 501, 502], "summary": {"covered_lines": 240, "num_statements": 261, "percent_covered": 89.21832884097034, "percent_covered_display": "89.22", "missing_lines": 21, "excluded_lines": 0, "num_branches": 110, "num_partial_branches": 11, "covered_branches": 91, "missing_branches": 19}, "missing_lines": [53, 130, 131, 132, 157, 216, 217, 218, 253, 300, 305, 306, 311, 312, 313, 318, 319, 373, 389, 390, 392], "excluded_lines": [], "executed_branches": [[27, -23], [27, 28], [52, 61], [101, 102], [101, 104], [121, 122], [121, 125], [123, 124], [123, 125], [129, 133], [133, 134], [133, 135], [156, 165], [203, 204], [215, 219], [219, 220], [219, 221], [244, 245], [244, 252], [252, 261], [286, 287], [286, 289], [310, 314], [314, 315], [314, 316], [329, 330], [329, 335], [359, 360], [359, 362], [362, 363], [362, 376], [366, 367], [366, 370], [368, 362], [368, 369], [370, 371], [378, 379], [378, 381], [381, 382], [381, 395], [385, 386], [387, 381], [387, 388], [396, 397], [396, 401], [398, 399], [398, 401], [411, 412], [411, 417], [417, 418], [417, 422], [425, 426], [425, 445], [426, 428], [426, 430], [431, 432], [431, 435], [445, 446], [445, 454], [447, 448], [447, 454], [449, 450], [449, 451], [451, 447], [451, 452], [454, -404], [454, 455], [456, -404], [456, 457], [458, 456], [458, 459], [469, 470], [469, 476], [477, 478], [477, 488], [478, 479], [478, 488], [479, 478], [479, 480], [480, 478], [480, 481], [481, 480], [481, 485], [489, 490], [490, 491], [490, 499], [491, 492], [492, 490], [492, 493], [493, 492], [493, 497]], "missing_branches": [[52, 53], [129, 130], [131, 132], [131, 133], [156, 157], [203, -141], [215, 216], [217, 218], [217, 219], [252, 253], [310, 311], [312, 313], [312, 314], [370, 373], [385, 389], [389, 390], [389, 392], [489, 499], [491, 490]]}}}, "src/toady/error_handling.py": {"executed_lines": [1, 8, 9, 10, 12, 30, 31, 34, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 54, 55, 56, 59, 60, 61, 62, 63, 64, 65, 68, 69, 70, 73, 74, 75, 78, 79, 82, 98, 113, 128, 145, 161, 177, 194, 211, 228, 246, 247, 249, 250, 260, 268, 269, 270, 273, 274, 275, 276, 277, 279, 280, 281, 282, 283, 285, 286, 287, 288, 289, 290, 291, 293, 294, 295, 296, 297, 299, 300, 301, 302, 303, 305, 307, 308, 309, 310, 311, 312, 315, 319, 320, 330, 346, 347, 348, 351, 352, 357, 360, 363, 371, 372, 375, 376, 378, 379, 382, 383, 386, 401, 403, 404, 405, 406, 408, 409, 410, 411, 413], "summary": {"covered_lines": 116, "num_statements": 116, "percent_covered": 96.875, "percent_covered_display": "96.88", "missing_lines": 0, "excluded_lines": 0, "num_branches": 44, "num_partial_branches": 5, "covered_branches": 39, "missing_branches": 5}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[269, 270], [269, 273], [273, 274], [273, 279], [275, 276], [279, 280], [279, 285], [281, 282], [285, 286], [285, 293], [287, 288], [287, 289], [289, 290], [289, 291], [293, 294], [293, 299], [295, 296], [299, 300], [299, 305], [301, 302], [305, 307], [305, 315], [308, 309], [310, 311], [310, 312], [347, 348], [347, 351], [351, 352], [351, 360], [375, 376], [375, 382], [403, 404], [403, 408], [405, 406], [405, 408], [408, 409], [408, 413], [410, 411], [410, 413]], "missing_branches": [[275, 277], [281, 283], [295, 297], [301, 303], [308, 312]], "functions": {"ErrorMessageFormatter.format_error": {"executed_lines": [260, 268, 269, 270, 273, 274, 275, 276, 277, 279, 280, 281, 282, 283, 285, 286, 287, 288, 289, 290, 291, 293, 294, 295, 296, 297, 299, 300, 301, 302, 303, 305, 307, 308, 309, 310, 311, 312, 315], "summary": {"covered_lines": 39, "num_statements": 39, "percent_covered": 92.7536231884058, "percent_covered_display": "92.75", "missing_lines": 0, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 5, "covered_branches": 25, "missing_branches": 5}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[269, 270], [269, 273], [273, 274], [273, 279], [275, 276], [279, 280], [279, 285], [281, 282], [285, 286], [285, 293], [287, 288], [287, 289], [289, 290], [289, 291], [293, 294], [293, 299], [295, 296], [299, 300], [299, 305], [301, 302], [305, 307], [305, 315], [308, 309], [310, 311], [310, 312]], "missing_branches": [[275, 277], [281, 283], [295, 297], [301, 303], [308, 312]]}, "ErrorMessageFormatter.get_exit_code": {"executed_lines": [330, 346, 347, 348, 351, 352, 357, 360], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[347, 348], [347, 351], [351, 352], [351, 360]], "missing_branches": []}, "handle_error": {"executed_lines": [371, 372, 375, 376, 378, 379, 382, 383], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[375, 376], [375, 382]], "missing_branches": []}, "create_user_friendly_error": {"executed_lines": [401, 403, 404, 405, 406, 408, 409, 410, 411, 413], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[403, 404], [403, 408], [405, 406], [405, 408], [408, 409], [408, 413], [410, 411], [410, 413]], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 10, 12, 30, 31, 34, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 54, 55, 56, 59, 60, 61, 62, 63, 64, 65, 68, 69, 70, 73, 74, 75, 78, 79, 82, 98, 113, 128, 145, 161, 177, 194, 211, 228, 246, 247, 249, 250, 319, 320, 363, 386], "summary": {"covered_lines": 51, "num_statements": 51, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ExitCode": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ErrorMessageTemplates": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ErrorMessageFormatter": {"executed_lines": [260, 268, 269, 270, 273, 274, 275, 276, 277, 279, 280, 281, 282, 283, 285, 286, 287, 288, 289, 290, 291, 293, 294, 295, 296, 297, 299, 300, 301, 302, 303, 305, 307, 308, 309, 310, 311, 312, 315, 330, 346, 347, 348, 351, 352, 357, 360], "summary": {"covered_lines": 47, "num_statements": 47, "percent_covered": 93.82716049382717, "percent_covered_display": "93.83", "missing_lines": 0, "excluded_lines": 0, "num_branches": 34, "num_partial_branches": 5, "covered_branches": 29, "missing_branches": 5}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[269, 270], [269, 273], [273, 274], [273, 279], [275, 276], [279, 280], [279, 285], [281, 282], [285, 286], [285, 293], [287, 288], [287, 289], [289, 290], [289, 291], [293, 294], [293, 299], [295, 296], [299, 300], [299, 305], [301, 302], [305, 307], [305, 315], [308, 309], [310, 311], [310, 312], [347, 348], [347, 351], [351, 352], [351, 360]], "missing_branches": [[275, 277], [281, 283], [295, 297], [301, 303], [308, 312]]}, "": {"executed_lines": [1, 8, 9, 10, 12, 30, 31, 34, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 54, 55, 56, 59, 60, 61, 62, 63, 64, 65, 68, 69, 70, 73, 74, 75, 78, 79, 82, 98, 113, 128, 145, 161, 177, 194, 211, 228, 246, 247, 249, 250, 319, 320, 363, 371, 372, 375, 376, 378, 379, 382, 383, 386, 401, 403, 404, 405, 406, 408, 409, 410, 411, 413], "summary": {"covered_lines": 69, "num_statements": 69, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[375, 376], [375, 382], [403, 404], [403, 408], [405, 406], [405, 408], [408, 409], [408, 413], [410, 411], [410, 413]], "missing_branches": []}}}, "src/toady/exceptions.py": {"executed_lines": [1, 7, 8, 11, 12, 14, 15, 16, 17, 20, 21, 24, 25, 26, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 58, 59, 60, 61, 64, 65, 66, 67, 68, 71, 72, 79, 96, 97, 98, 99, 100, 101, 103, 105, 107, 113, 123, 124, 131, 148, 154, 155, 156, 159, 160, 161, 162, 163, 164, 167, 168, 174, 184, 190, 191, 192, 195, 196, 201, 216, 222, 223, 224, 225, 226, 227, 230, 231, 237, 252, 258, 259, 260, 261, 262, 263, 266, 267, 273, 280, 288, 289, 291, 300, 313, 314, 316, 325, 338, 339, 341, 356, 362, 363, 364, 365, 366, 367, 370, 371, 373, 386, 397, 398, 399, 402, 403, 405, 418, 429, 430, 431, 434, 435, 437, 452, 458, 459, 460, 461, 462, 463, 466, 467, 469, 484, 495, 496, 497, 498, 499, 500, 503, 504, 506, 523, 529, 530, 531, 532, 533, 534, 535, 536, 537, 541, 542, 544, 551, 559, 560, 562, 569, 576, 577, 579, 589, 599, 600, 601, 604, 605, 607, 614, 621, 622, 624, 634, 644, 645, 646, 649, 650, 652, 662, 672, 673, 674, 679, 682, 699, 700, 704, 712, 727], "summary": {"covered_lines": 183, "num_statements": 183, "percent_covered": 99.56331877729258, "percent_covered_display": "99.56", "missing_lines": 0, "excluded_lines": 0, "num_branches": 46, "num_partial_branches": 1, "covered_branches": 45, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[159, 160], [159, 161], [161, 162], [161, 163], [163, -131], [163, 164], [191, -174], [191, 192], [224, 225], [224, 226], [226, -201], [226, 227], [260, 261], [260, 262], [262, -237], [262, 263], [364, 365], [364, 366], [366, -341], [366, 367], [398, -373], [398, 399], [430, -405], [430, 431], [460, 461], [460, 462], [462, -437], [462, 463], [497, 498], [497, 499], [499, -469], [499, 500], [532, 533], [532, 534], [534, 535], [534, 536], [536, -506], [536, 537], [600, 601], [645, -624], [645, 646], [673, -652], [673, 674], [699, 700], [699, 704]], "missing_branches": [[600, -579]], "functions": {"ToadyError.__init__": {"executed_lines": [96, 97, 98, 99, 100, 101], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ToadyError.__str__": {"executed_lines": [105], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ToadyError.to_dict": {"executed_lines": [113], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ValidationError.__init__": {"executed_lines": [148, 154, 155, 156, 159, 160, 161, 162, 163, 164], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[159, 160], [159, 161], [161, 162], [161, 163], [163, -131], [163, 164]], "missing_branches": []}, "ConfigurationError.__init__": {"executed_lines": [184, 190, 191, 192], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[191, -174], [191, 192]], "missing_branches": []}, "FileOperationError.__init__": {"executed_lines": [216, 222, 223, 224, 225, 226, 227], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[224, 225], [224, 226], [226, -201], [226, 227]], "missing_branches": []}, "NetworkError.__init__": {"executed_lines": [252, 258, 259, 260, 261, 262, 263], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[260, 261], [260, 262], [262, -237], [262, 263]], "missing_branches": []}, "GitHubServiceError.__init__": {"executed_lines": [280], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubCLINotFoundError.__init__": {"executed_lines": [300], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAuthenticationError.__init__": {"executed_lines": [325], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAPIError.__init__": {"executed_lines": [356, 362, 363, 364, 365, 366, 367], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[364, 365], [364, 366], [366, -341], [366, 367]], "missing_branches": []}, "GitHubTimeoutError.__init__": {"executed_lines": [386, 397, 398, 399], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[398, -373], [398, 399]], "missing_branches": []}, "GitHubRateLimitError.__init__": {"executed_lines": [418, 429, 430, 431], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[430, -405], [430, 431]], "missing_branches": []}, "GitHubNotFoundError.__init__": {"executed_lines": [452, 458, 459, 460, 461, 462, 463], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[460, 461], [460, 462], [462, -437], [462, 463]], "missing_branches": []}, "GitHubPermissionError.__init__": {"executed_lines": [484, 495, 496, 497, 498, 499, 500], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[497, 498], [497, 499], [499, -469], [499, 500]], "missing_branches": []}, "CommandExecutionError.__init__": {"executed_lines": [523, 529, 530, 531, 532, 533, 534, 535, 536, 537], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[532, 533], [532, 534], [534, 535], [534, 536], [536, -506], [536, 537]], "missing_branches": []}, "FetchServiceError.__init__": {"executed_lines": [551], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyServiceError.__init__": {"executed_lines": [569], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "CommentNotFoundError.__init__": {"executed_lines": [589, 599, 600, 601], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[600, 601]], "missing_branches": [[600, -579]]}, "ResolveServiceError.__init__": {"executed_lines": [614], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ThreadNotFoundError.__init__": {"executed_lines": [634, 644, 645, 646], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[645, -624], [645, 646]], "missing_branches": []}, "ThreadPermissionError.__init__": {"executed_lines": [662, 672, 673, 674], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[673, -652], [673, 674]], "missing_branches": []}, "create_validation_error": {"executed_lines": [699, 700, 704], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[699, 700], [699, 704]], "missing_branches": []}, "create_github_error": {"executed_lines": [727], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 11, 12, 14, 15, 16, 17, 20, 21, 24, 25, 26, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 58, 59, 60, 61, 64, 65, 66, 67, 68, 71, 72, 79, 103, 107, 123, 124, 131, 167, 168, 174, 195, 196, 201, 230, 231, 237, 266, 267, 273, 288, 289, 291, 313, 314, 316, 338, 339, 341, 370, 371, 373, 402, 403, 405, 434, 435, 437, 466, 467, 469, 503, 504, 506, 541, 542, 544, 559, 560, 562, 576, 577, 579, 604, 605, 607, 621, 622, 624, 649, 650, 652, 679, 682, 712], "summary": {"covered_lines": 86, "num_statements": 86, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ErrorSeverity": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ErrorCode": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ToadyError": {"executed_lines": [96, 97, 98, 99, 100, 101, 105, 113], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ValidationError": {"executed_lines": [148, 154, 155, 156, 159, 160, 161, 162, 163, 164], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[159, 160], [159, 161], [161, 162], [161, 163], [163, -131], [163, 164]], "missing_branches": []}, "ConfigurationError": {"executed_lines": [184, 190, 191, 192], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[191, -174], [191, 192]], "missing_branches": []}, "FileOperationError": {"executed_lines": [216, 222, 223, 224, 225, 226, 227], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[224, 225], [224, 226], [226, -201], [226, 227]], "missing_branches": []}, "NetworkError": {"executed_lines": [252, 258, 259, 260, 261, 262, 263], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[260, 261], [260, 262], [262, -237], [262, 263]], "missing_branches": []}, "GitHubServiceError": {"executed_lines": [280], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubCLINotFoundError": {"executed_lines": [300], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAuthenticationError": {"executed_lines": [325], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAPIError": {"executed_lines": [356, 362, 363, 364, 365, 366, 367], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[364, 365], [364, 366], [366, -341], [366, 367]], "missing_branches": []}, "GitHubTimeoutError": {"executed_lines": [386, 397, 398, 399], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[398, -373], [398, 399]], "missing_branches": []}, "GitHubRateLimitError": {"executed_lines": [418, 429, 430, 431], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[430, -405], [430, 431]], "missing_branches": []}, "GitHubNotFoundError": {"executed_lines": [452, 458, 459, 460, 461, 462, 463], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[460, 461], [460, 462], [462, -437], [462, 463]], "missing_branches": []}, "GitHubPermissionError": {"executed_lines": [484, 495, 496, 497, 498, 499, 500], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[497, 498], [497, 499], [499, -469], [499, 500]], "missing_branches": []}, "CommandExecutionError": {"executed_lines": [523, 529, 530, 531, 532, 533, 534, 535, 536, 537], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[532, 533], [532, 534], [534, 535], [534, 536], [536, -506], [536, 537]], "missing_branches": []}, "FetchServiceError": {"executed_lines": [551], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyServiceError": {"executed_lines": [569], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "CommentNotFoundError": {"executed_lines": [589, 599, 600, 601], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[600, 601]], "missing_branches": [[600, -579]]}, "ResolveServiceError": {"executed_lines": [614], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ThreadNotFoundError": {"executed_lines": [634, 644, 645, 646], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[645, -624], [645, 646]], "missing_branches": []}, "ThreadPermissionError": {"executed_lines": [662, 672, 673, 674], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[673, -652], [673, 674]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 11, 12, 14, 15, 16, 17, 20, 21, 24, 25, 26, 29, 30, 31, 32, 33, 36, 37, 38, 39, 40, 43, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 58, 59, 60, 61, 64, 65, 66, 67, 68, 71, 72, 79, 103, 107, 123, 124, 131, 167, 168, 174, 195, 196, 201, 230, 231, 237, 266, 267, 273, 288, 289, 291, 313, 314, 316, 338, 339, 341, 370, 371, 373, 402, 403, 405, 434, 435, 437, 466, 467, 469, 503, 504, 506, 541, 542, 544, 559, 560, 562, 576, 577, 579, 604, 605, 607, 621, 622, 624, 649, 650, 652, 679, 682, 699, 700, 704, 712, 727], "summary": {"covered_lines": 90, "num_statements": 90, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[699, 700], [699, 704]], "missing_branches": []}}}, "src/toady/formatters/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/formatters/format_interfaces.py": {"executed_lines": [1, 8, 9, 11, 14, 15, 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 111, 130, 131, 137, 143, 145, 154, 155, 156, 157, 158, 159, 160, 161, 164, 165, 167, 168, 169, 170, 172, 184, 185, 186, 188, 190, 200, 202, 206, 207, 208, 209, 211, 215, 216, 217, 218, 221, 222, 224, 233, 234, 237, 238, 240, 257, 258, 259, 260, 261, 263, 269, 278, 279, 281, 283, 284, 291, 293, 294, 307, 308, 309, 313, 314, 315, 316, 317, 318, 322, 323, 329, 331, 332, 341], "summary": {"covered_lines": 76, "num_statements": 83, "percent_covered": 88.57142857142857, "percent_covered_display": "88.57", "missing_lines": 7, "excluded_lines": 66, "num_branches": 22, "num_partial_branches": 1, "covered_branches": 17, "missing_branches": 5}, "missing_lines": [105, 106, 109, 123, 124, 127, 187], "excluded_lines": [21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91], "executed_branches": [[154, 155], [154, 156], [156, 157], [156, 158], [158, 159], [158, 160], [160, 161], [160, 164], [184, 185], [184, 186], [186, 188], [207, 208], [207, 209], [216, 217], [216, 218], [307, 308], [307, 313]], "missing_branches": [[105, 106], [105, 109], [123, 124], [123, 127], [186, 187]], "functions": {"IFormatter.format_threads": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [23, 24, 25, 26, 27, 28, 29, 30, 31], "executed_branches": [], "missing_branches": []}, "IFormatter.format_comments": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [35, 36, 37, 38, 39, 40, 41, 42, 43], "executed_branches": [], "missing_branches": []}, "IFormatter.format_object": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [47, 48, 49, 50, 51, 52, 53, 54, 55], "executed_branches": [], "missing_branches": []}, "IFormatter.format_array": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [59, 60, 61, 62, 63, 64, 65, 66, 67], "executed_branches": [], "missing_branches": []}, "IFormatter.format_primitive": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [71, 72, 73, 74, 75, 76, 77, 78, 79], "executed_branches": [], "missing_branches": []}, "IFormatter.format_error": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 9, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [83, 84, 85, 86, 87, 88, 89, 90, 91], "executed_branches": [], "missing_branches": []}, "IFormatter.format_success_message": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [105, 106, 109], "excluded_lines": [], "executed_branches": [], "missing_branches": [[105, 106], [105, 109]]}, "IFormatter.format_warning_message": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [123, 124, 127], "excluded_lines": [], "executed_branches": [], "missing_branches": [[123, 124], [123, 127]]}, "BaseFormatter.__init__": {"executed_lines": [143], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "BaseFormatter._safe_serialize": {"executed_lines": [154, 155, 156, 157, 158, 159, 160, 161, 164, 165, 167, 168, 169, 170], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[154, 155], [154, 156], [156, 157], [156, 158], [158, 159], [158, 160], [160, 161], [160, 164]], "missing_branches": []}, "BaseFormatter._handle_empty_data": {"executed_lines": [184, 185, 186, 188], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 77.77777777777777, "percent_covered_display": "77.78", "missing_lines": 1, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [187], "excluded_lines": [], "executed_branches": [[184, 185], [184, 186], [186, 188]], "missing_branches": [[186, 187]]}, "BaseFormatter.format_comments": {"executed_lines": [200], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "BaseFormatter.format_success_message": {"executed_lines": [206, 207, 208, 209], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[207, 208], [207, 209]], "missing_branches": []}, "BaseFormatter.format_warning_message": {"executed_lines": [215, 216, 217, 218], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[216, 217], [216, 218]], "missing_branches": []}, "FormatterError.__init__": {"executed_lines": [233, 234], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterOptions.__init__": {"executed_lines": [257, 258, 259, 260, 261], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterOptions.to_dict": {"executed_lines": [269], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory.register": {"executed_lines": [291], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory.create": {"executed_lines": [307, 308, 309, 313, 314, 315, 316, 317, 318], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[307, 308], [307, 313]], "missing_branches": []}, "FormatterFactory.list_formatters": {"executed_lines": [329], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory.is_registered": {"executed_lines": [341], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 11, 14, 15, 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 111, 130, 131, 137, 145, 172, 190, 202, 211, 221, 222, 224, 237, 238, 240, 263, 278, 279, 281, 283, 284, 293, 294, 322, 323, 331, 332], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 12, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82], "executed_branches": [], "missing_branches": []}}, "classes": {"IFormatter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 6, "excluded_lines": 54, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 4}, "missing_lines": [105, 106, 109, 123, 124, 127], "excluded_lines": [23, 24, 25, 26, 27, 28, 29, 30, 31, 35, 36, 37, 38, 39, 40, 41, 42, 43, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59, 60, 61, 62, 63, 64, 65, 66, 67, 71, 72, 73, 74, 75, 76, 77, 78, 79, 83, 84, 85, 86, 87, 88, 89, 90, 91], "executed_branches": [], "missing_branches": [[105, 106], [105, 109], [123, 124], [123, 127]]}, "BaseFormatter": {"executed_lines": [143, 154, 155, 156, 157, 158, 159, 160, 161, 164, 165, 167, 168, 169, 170, 184, 185, 186, 188, 200, 206, 207, 208, 209, 215, 216, 217, 218], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 95.55555555555556, "percent_covered_display": "95.56", "missing_lines": 1, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 1, "covered_branches": 15, "missing_branches": 1}, "missing_lines": [187], "excluded_lines": [], "executed_branches": [[154, 155], [154, 156], [156, 157], [156, 158], [158, 159], [158, 160], [160, 161], [160, 164], [184, 185], [184, 186], [186, 188], [207, 208], [207, 209], [216, 217], [216, 218]], "missing_branches": [[186, 187]]}, "FormatterError": {"executed_lines": [233, 234], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterOptions": {"executed_lines": [257, 258, 259, 260, 261, 269], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FormatterFactory": {"executed_lines": [291, 307, 308, 309, 313, 314, 315, 316, 317, 318, 329, 341], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[307, 308], [307, 313]], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 11, 14, 15, 21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82, 93, 111, 130, 131, 137, 145, 172, 190, 202, 211, 221, 222, 224, 237, 238, 240, 263, 278, 279, 281, 283, 284, 293, 294, 322, 323, 331, 332], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 12, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [21, 22, 33, 34, 45, 46, 57, 58, 69, 70, 81, 82], "executed_branches": [], "missing_branches": []}}}, "src/toady/formatters/format_selection.py": {"executed_lines": [1, 7, 8, 10, 14, 15, 16, 20, 26, 29, 30, 31, 33, 34, 36, 38, 39, 40, 44, 45, 49, 50, 53, 54, 57, 58, 61, 62, 65, 66, 74, 75, 83, 86, 87, 88, 90, 91, 92, 96, 99, 102, 103, 105, 112, 113, 116, 123, 125, 126, 129, 132, 145, 147, 149, 150, 156, 159, 173, 174, 177, 178, 181, 184, 198, 200, 201, 202, 203, 208, 217, 218, 220, 228, 230, 233, 242, 249, 251, 257, 265, 267, 268, 270, 273, 274, 275, 278, 285, 286, 288, 289, 291, 292, 293, 296, 297, 298, 301, 311, 312, 314, 315, 316, 317, 320, 321, 322, 325, 332, 333, 335, 338, 339, 340], "summary": {"covered_lines": 113, "num_statements": 129, "percent_covered": 87.42138364779875, "percent_covered_display": "87.42", "missing_lines": 16, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 0, "covered_branches": 26, "missing_branches": 4}, "missing_lines": [41, 42, 46, 47, 51, 55, 59, 63, 69, 70, 71, 72, 78, 79, 80, 81], "excluded_lines": [], "executed_branches": [[29, 30], [29, 86], [86, -20], [86, 87], [125, 126], [125, 129], [149, 150], [149, 156], [173, 174], [173, 177], [177, 178], [177, 181], [265, 267], [265, 268], [268, 270], [268, 273], [285, 286], [285, 289], [289, 291], [289, 296], [311, 312], [311, 320], [315, 316], [315, 317], [332, 333], [332, 338]], "missing_branches": [[70, 71], [70, 72], [79, 80], [79, 81]], "functions": {"_ensure_formatters_registered": {"executed_lines": [26, 29, 30, 31, 33, 34, 36, 38, 39, 40, 44, 45, 49, 50, 53, 54, 57, 58, 61, 62, 65, 66, 74, 75, 83, 86, 87, 88, 90, 91, 92], "summary": {"covered_lines": 31, "num_statements": 47, "percent_covered": 63.63636363636363, "percent_covered_display": "63.64", "missing_lines": 16, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 4}, "missing_lines": [41, 42, 46, 47, 51, 55, 59, 63, 69, 70, 71, 72, 78, 79, 80, 81], "excluded_lines": [], "executed_branches": [[29, 30], [29, 86], [86, -20], [86, 87]], "missing_branches": [[70, 71], [70, 72], [79, 80], [79, 81]]}, "FormatSelectionError.__init__": {"executed_lines": [112, 113], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "get_default_format": {"executed_lines": [123, 125, 126, 129], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[125, 126], [125, 129]], "missing_branches": []}, "validate_format": {"executed_lines": [145, 147, 149, 150, 156], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[149, 150], [149, 156]], "missing_branches": []}, "resolve_format_from_options": {"executed_lines": [173, 174, 177, 178, 181], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[173, 174], [173, 177], [177, 178], [177, 181]], "missing_branches": []}, "create_formatter": {"executed_lines": [198, 200, 201, 202, 203], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_format_option": {"executed_lines": [217, 218, 220, 228, 230], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_legacy_pretty_option": {"executed_lines": [242, 249, 251], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_threads_output": {"executed_lines": [265, 267, 268, 270, 273, 274, 275], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[265, 267], [265, 268], [268, 270], [268, 273]], "missing_branches": []}, "format_object_output": {"executed_lines": [285, 286, 288, 289, 291, 292, 293, 296, 297, 298], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[285, 286], [285, 289], [289, 291], [289, 296]], "missing_branches": []}, "format_success_message": {"executed_lines": [311, 312, 314, 315, 316, 317, 320, 321, 322], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[311, 312], [311, 320], [315, 316], [315, 317]], "missing_branches": []}, "format_error_message": {"executed_lines": [332, 333, 335, 338, 339, 340], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[332, 333], [332, 338]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 10, 14, 15, 16, 20, 96, 99, 102, 103, 105, 116, 132, 159, 184, 208, 233, 257, 278, 301, 325], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"FormatSelectionError": {"executed_lines": [112, 113], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 10, 14, 15, 16, 20, 26, 29, 30, 31, 33, 34, 36, 38, 39, 40, 44, 45, 49, 50, 53, 54, 57, 58, 61, 62, 65, 66, 74, 75, 83, 86, 87, 88, 90, 91, 92, 96, 99, 102, 103, 105, 116, 123, 125, 126, 129, 132, 145, 147, 149, 150, 156, 159, 173, 174, 177, 178, 181, 184, 198, 200, 201, 202, 203, 208, 217, 218, 220, 228, 230, 233, 242, 249, 251, 257, 265, 267, 268, 270, 273, 274, 275, 278, 285, 286, 288, 289, 291, 292, 293, 296, 297, 298, 301, 311, 312, 314, 315, 316, 317, 320, 321, 322, 325, 332, 333, 335, 338, 339, 340], "summary": {"covered_lines": 111, "num_statements": 127, "percent_covered": 87.26114649681529, "percent_covered_display": "87.26", "missing_lines": 16, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 0, "covered_branches": 26, "missing_branches": 4}, "missing_lines": [41, 42, 46, 47, 51, 55, 59, 63, 69, 70, 71, 72, 78, 79, 80, 81], "excluded_lines": [], "executed_branches": [[29, 30], [29, 86], [86, -20], [86, 87], [125, 126], [125, 129], [149, 150], [149, 156], [173, 174], [173, 177], [177, 178], [177, 181], [265, 267], [265, 268], [268, 270], [268, 273], [285, 286], [285, 289], [289, 291], [289, 296], [311, 312], [311, 320], [315, 316], [315, 317], [332, 333], [332, 338]], "missing_branches": [[70, 71], [70, 72], [79, 80], [79, 81]]}}}, "src/toady/formatters/formatters.py": {"executed_lines": [1, 7, 8, 9, 11, 13, 14, 15, 18, 19, 21, 22, 32, 33, 35, 38, 39, 41, 42, 51, 52, 55, 56, 58, 59, 70, 71, 73, 81, 82, 84, 85, 86, 90, 92, 93, 102, 103, 105, 108, 111, 112, 113, 115, 118, 119, 120, 123, 124, 126, 128, 129, 142, 145, 146, 149, 151, 152, 154, 156, 159, 161, 162, 164, 165, 168, 173, 174, 175, 176, 177, 182, 185, 187, 188, 197, 198, 200, 203, 204, 206, 208, 209, 210, 211, 215, 216, 219, 220, 223, 224, 228, 229, 230, 233, 236, 239, 240, 243, 244, 245, 248, 250, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 267, 271, 272, 273, 274, 275, 276, 277, 280, 282, 283, 294, 296, 297, 307, 310, 328, 330, 331, 334, 337, 338, 341, 342, 345, 348, 349, 353, 356, 357, 359], "summary": {"covered_lines": 140, "num_statements": 153, "percent_covered": 89.14027149321267, "percent_covered_display": "89.14", "missing_lines": 13, "excluded_lines": 0, "num_branches": 68, "num_partial_branches": 11, "covered_branches": 57, "missing_branches": 11}, "missing_lines": [88, 147, 166, 167, 170, 179, 183, 212, 213, 225, 278, 360, 362], "excluded_lines": [], "executed_branches": [[32, 33], [32, 35], [70, 71], [70, 73], [84, 85], [84, 90], [85, 86], [102, 103], [102, 105], [111, 112], [111, 118], [112, 113], [112, 115], [118, 119], [118, 123], [123, 124], [123, 126], [146, 149], [151, 152], [151, 154], [159, 161], [164, 165], [164, 182], [165, 168], [168, 173], [173, 174], [175, 164], [175, 176], [176, 177], [182, 185], [197, 198], [197, 200], [206, 208], [206, 265], [208, 209], [208, 211], [211, 215], [224, 228], [243, 244], [243, 260], [250, 251], [250, 260], [256, 250], [256, 257], [260, 206], [260, 261], [273, 274], [273, 275], [275, 276], [275, 277], [277, 280], [328, 330], [328, 348], [330, 331], [330, 337], [341, -310], [341, 342]], "missing_branches": [[85, 88], [146, 147], [159, 182], [165, 166], [168, 170], [173, 179], [176, 175], [182, 183], [211, 212], [224, 225], [277, 278]], "functions": {"OutputFormatter.format_threads": {"executed_lines": [32, 33, 35], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[32, 33], [32, 35]], "missing_branches": []}, "JSONFormatter.format_threads": {"executed_lines": [51, 52], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._wrap_text": {"executed_lines": [70, 71, 73, 81, 82, 84, 85, 86, 90], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 87.5, "percent_covered_display": "87.50", "missing_lines": 1, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [88], "excluded_lines": [], "executed_branches": [[70, 71], [70, 73], [84, 85], [84, 90], [85, 86]], "missing_branches": [[85, 88]]}, "PrettyFormatter._format_file_context": {"executed_lines": [102, 103, 105, 108, 111, 112, 113, 115, 118, 119, 120, 123, 124, 126], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[102, 103], [102, 105], [111, 112], [111, 118], [112, 113], [112, 115], [118, 119], [118, 123], [123, 124], [123, 126]], "missing_branches": []}, "PrettyFormatter._format_comment": {"executed_lines": [142, 145, 146, 149, 151, 152, 154, 156, 159, 161, 162, 164, 165, 168, 173, 174, 175, 176, 177, 182, 185], "summary": {"covered_lines": 21, "num_statements": 27, "percent_covered": 72.34042553191489, "percent_covered_display": "72.34", "missing_lines": 6, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 7, "covered_branches": 13, "missing_branches": 7}, "missing_lines": [147, 166, 167, 170, 179, 183], "excluded_lines": [], "executed_branches": [[146, 149], [151, 152], [151, 154], [159, 161], [164, 165], [164, 182], [165, 168], [168, 173], [173, 174], [175, 164], [175, 176], [176, 177], [182, 185]], "missing_branches": [[146, 147], [159, 182], [165, 166], [168, 170], [173, 179], [176, 175], [182, 183]]}, "PrettyFormatter.format_threads": {"executed_lines": [197, 198, 200, 203, 204, 206, 208, 209, 210, 211, 215, 216, 219, 220, 223, 224, 228, 229, 230, 233, 236, 239, 240, 243, 244, 245, 248, 250, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 267, 271, 272, 273, 274, 275, 276, 277, 280], "summary": {"covered_lines": 47, "num_statements": 51, "percent_covered": 90.66666666666667, "percent_covered_display": "90.67", "missing_lines": 4, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 3, "covered_branches": 21, "missing_branches": 3}, "missing_lines": [212, 213, 225, 278], "excluded_lines": [], "executed_branches": [[197, 198], [197, 200], [206, 208], [206, 265], [208, 209], [208, 211], [211, 215], [224, 228], [243, 244], [243, 260], [250, 251], [250, 260], [256, 250], [256, 257], [260, 206], [260, 261], [273, 274], [273, 275], [275, 276], [275, 277], [277, 280]], "missing_branches": [[211, 212], [224, 225], [277, 278]]}, "PrettyFormatter.format_progress_message": {"executed_lines": [294], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter.format_result_summary": {"executed_lines": [307], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_fetch_output": {"executed_lines": [328, 330, 331, 334, 337, 338, 341, 342, 345, 348, 349], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[328, 330], [328, 348], [330, 331], [330, 337], [341, -310], [341, 342]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 11, 13, 14, 15, 18, 19, 21, 22, 38, 39, 41, 42, 55, 56, 58, 59, 92, 93, 128, 129, 187, 188, 282, 283, 296, 297, 310, 353, 356, 357, 359], "summary": {"covered_lines": 31, "num_statements": 33, "percent_covered": 93.93939393939394, "percent_covered_display": "93.94", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [360, 362], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"OutputFormatter": {"executed_lines": [32, 33, 35], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[32, 33], [32, 35]], "missing_branches": []}, "JSONFormatter": {"executed_lines": [51, 52], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter": {"executed_lines": [70, 71, 73, 81, 82, 84, 85, 86, 90, 102, 103, 105, 108, 111, 112, 113, 115, 118, 119, 120, 123, 124, 126, 142, 145, 146, 149, 151, 152, 154, 156, 159, 161, 162, 164, 165, 168, 173, 174, 175, 176, 177, 182, 185, 197, 198, 200, 203, 204, 206, 208, 209, 210, 211, 215, 216, 219, 220, 223, 224, 228, 229, 230, 233, 236, 239, 240, 243, 244, 245, 248, 250, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 267, 271, 272, 273, 274, 275, 276, 277, 280, 294, 307], "summary": {"covered_lines": 93, "num_statements": 104, "percent_covered": 86.58536585365853, "percent_covered_display": "86.59", "missing_lines": 11, "excluded_lines": 0, "num_branches": 60, "num_partial_branches": 11, "covered_branches": 49, "missing_branches": 11}, "missing_lines": [88, 147, 166, 167, 170, 179, 183, 212, 213, 225, 278], "excluded_lines": [], "executed_branches": [[70, 71], [70, 73], [84, 85], [84, 90], [85, 86], [102, 103], [102, 105], [111, 112], [111, 118], [112, 113], [112, 115], [118, 119], [118, 123], [123, 124], [123, 126], [146, 149], [151, 152], [151, 154], [159, 161], [164, 165], [164, 182], [165, 168], [168, 173], [173, 174], [175, 164], [175, 176], [176, 177], [182, 185], [197, 198], [197, 200], [206, 208], [206, 265], [208, 209], [208, 211], [211, 215], [224, 228], [243, 244], [243, 260], [250, 251], [250, 260], [256, 250], [256, 257], [260, 206], [260, 261], [273, 274], [273, 275], [275, 276], [275, 277], [277, 280]], "missing_branches": [[85, 88], [146, 147], [159, 182], [165, 166], [168, 170], [173, 179], [176, 175], [182, 183], [211, 212], [224, 225], [277, 278]]}, "": {"executed_lines": [1, 7, 8, 9, 11, 13, 14, 15, 18, 19, 21, 22, 38, 39, 41, 42, 55, 56, 58, 59, 92, 93, 128, 129, 187, 188, 282, 283, 296, 297, 310, 328, 330, 331, 334, 337, 338, 341, 342, 345, 348, 349, 353, 356, 357, 359], "summary": {"covered_lines": 42, "num_statements": 44, "percent_covered": 96.0, "percent_covered_display": "96.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [360, 362], "excluded_lines": [], "executed_branches": [[328, 330], [328, 348], [330, 331], [330, 337], [341, -310], [341, 342]], "missing_branches": []}}}, "src/toady/formatters/json_formatter.py": {"executed_lines": [1, 8, 9, 11, 12, 15, 16, 22, 31, 32, 35, 41, 42, 44, 56, 58, 59, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 77, 79, 80, 81, 82, 86, 98, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 119, 121, 122, 123, 128, 140, 141, 142, 143, 144, 148, 160, 162, 163, 166, 167, 168, 169, 170, 172, 173, 174, 175, 180, 182, 183, 184, 189, 201, 202, 208, 220, 222, 223, 224, 225, 226, 228, 234, 246, 248, 249, 251, 253, 265, 267, 268, 270, 272, 284, 293, 304, 305, 306, 309, 310, 312, 314, 323, 325, 335, 336, 339, 340, 343, 344, 345, 350, 351, 352, 353, 355, 358, 359, 362, 363, 366, 367, 370, 371, 372, 389, 392, 401, 404, 413, 416, 425], "summary": {"covered_lines": 132, "num_statements": 150, "percent_covered": 89.80582524271844, "percent_covered_display": "89.81", "missing_lines": 18, "excluded_lines": 0, "num_branches": 56, "num_partial_branches": 3, "covered_branches": 53, "missing_branches": 3}, "missing_lines": [124, 185, 203, 204, 229, 230, 346, 347, 373, 375, 376, 377, 378, 381, 382, 383, 384, 385], "excluded_lines": [], "executed_branches": [[41, -22], [41, 42], [58, 59], [58, 62], [63, 64], [63, 77], [65, 66], [65, 68], [80, 81], [80, 82], [100, 101], [100, 104], [105, 106], [105, 119], [107, 108], [107, 110], [122, 123], [162, 163], [162, 166], [167, 168], [167, 180], [169, 170], [169, 172], [183, 184], [223, 224], [223, 225], [225, 226], [225, 228], [248, 249], [248, 251], [267, 268], [267, 270], [304, 305], [304, 309], [305, 304], [305, 306], [309, 310], [309, 312], [335, 336], [335, 339], [339, 340], [339, 343], [343, 344], [343, 350], [350, 351], [350, 358], [358, 359], [358, 362], [362, 363], [362, 366], [366, 367], [366, 370], [370, 371]], "missing_branches": [[122, 124], [183, 185], [370, 381]], "functions": {"JSONFormatter.__init__": {"executed_lines": [31, 32, 35, 41, 42], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[41, -22], [41, 42]], "missing_branches": []}, "JSONFormatter.format_threads": {"executed_lines": [56, 58, 59, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 77, 79, 80, 81, 82], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[58, 59], [58, 62], [63, 64], [63, 77], [65, 66], [65, 68], [80, 81], [80, 82]], "missing_branches": []}, "JSONFormatter.format_comments": {"executed_lines": [98, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 119, 121, 122, 123], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 92.3076923076923, "percent_covered_display": "92.31", "missing_lines": 1, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [124], "excluded_lines": [], "executed_branches": [[100, 101], [100, 104], [105, 106], [105, 119], [107, 108], [107, 110], [122, 123]], "missing_branches": [[122, 124]]}, "JSONFormatter.format_object": {"executed_lines": [140, 141, 142, 143, 144], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "JSONFormatter.format_array": {"executed_lines": [160, 162, 163, 166, 167, 168, 169, 170, 172, 173, 174, 175, 180, 182, 183, 184], "summary": {"covered_lines": 16, "num_statements": 17, "percent_covered": 92.0, "percent_covered_display": "92.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [185], "excluded_lines": [], "executed_branches": [[162, 163], [162, 166], [167, 168], [167, 180], [169, 170], [169, 172], [183, 184]], "missing_branches": [[183, 185]]}, "JSONFormatter.format_primitive": {"executed_lines": [201, 202], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [203, 204], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "JSONFormatter.format_error": {"executed_lines": [220, 222, 223, 224, 225, 226, 228], "summary": {"covered_lines": 7, "num_statements": 9, "percent_covered": 84.61538461538461, "percent_covered_display": "84.62", "missing_lines": 2, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [229, 230], "excluded_lines": [], "executed_branches": [[223, 224], [223, 225], [225, 226], [225, 228]], "missing_branches": []}, "JSONFormatter.format_success_message": {"executed_lines": [246, 248, 249, 251], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[248, 249], [248, 251]], "missing_branches": []}, "JSONFormatter.format_warning_message": {"executed_lines": [265, 267, 268, 270], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[267, 268], [267, 270]], "missing_branches": []}, "JSONFormatter.format_reply_result": {"executed_lines": [284, 293, 304, 305, 306, 309, 310, 312], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[304, 305], [304, 309], [305, 304], [305, 306], [309, 310], [309, 312]], "missing_branches": []}, "JSONFormatter.format_resolve_result": {"executed_lines": [323], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "JSONFormatter._safe_serialize": {"executed_lines": [335, 336, 339, 340, 343, 344, 345, 350, 351, 352, 353, 355, 358, 359, 362, 363, 366, 367, 370, 371, 372], "summary": {"covered_lines": 21, "num_statements": 33, "percent_covered": 73.46938775510205, "percent_covered_display": "73.47", "missing_lines": 12, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 1, "covered_branches": 15, "missing_branches": 1}, "missing_lines": [346, 347, 373, 375, 376, 377, 378, 381, 382, 383, 384, 385], "excluded_lines": [], "executed_branches": [[335, 336], [335, 339], [339, 340], [339, 343], [343, 344], [343, 350], [350, 351], [350, 358], [358, 359], [358, 362], [362, 363], [362, 366], [366, 367], [366, 370], [370, 371]], "missing_branches": [[370, 381]]}, "format_threads_json": {"executed_lines": [401], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_comments_json": {"executed_lines": [413], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_object_json": {"executed_lines": [425], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 11, 12, 15, 16, 22, 44, 86, 128, 148, 189, 208, 234, 253, 272, 314, 325, 389, 392, 404, 416], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"JSONFormatter": {"executed_lines": [31, 32, 35, 41, 42, 56, 58, 59, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 77, 79, 80, 81, 82, 98, 100, 101, 104, 105, 106, 107, 108, 110, 111, 112, 113, 114, 119, 121, 122, 123, 140, 141, 142, 143, 144, 160, 162, 163, 166, 167, 168, 169, 170, 172, 173, 174, 175, 180, 182, 183, 184, 201, 202, 220, 222, 223, 224, 225, 226, 228, 246, 248, 249, 251, 265, 267, 268, 270, 284, 293, 304, 305, 306, 309, 310, 312, 323, 335, 336, 339, 340, 343, 344, 345, 350, 351, 352, 353, 355, 358, 359, 362, 363, 366, 367, 370, 371, 372], "summary": {"covered_lines": 108, "num_statements": 126, "percent_covered": 88.46153846153847, "percent_covered_display": "88.46", "missing_lines": 18, "excluded_lines": 0, "num_branches": 56, "num_partial_branches": 3, "covered_branches": 53, "missing_branches": 3}, "missing_lines": [124, 185, 203, 204, 229, 230, 346, 347, 373, 375, 376, 377, 378, 381, 382, 383, 384, 385], "excluded_lines": [], "executed_branches": [[41, -22], [41, 42], [58, 59], [58, 62], [63, 64], [63, 77], [65, 66], [65, 68], [80, 81], [80, 82], [100, 101], [100, 104], [105, 106], [105, 119], [107, 108], [107, 110], [122, 123], [162, 163], [162, 166], [167, 168], [167, 180], [169, 170], [169, 172], [183, 184], [223, 224], [223, 225], [225, 226], [225, 228], [248, 249], [248, 251], [267, 268], [267, 270], [304, 305], [304, 309], [305, 304], [305, 306], [309, 310], [309, 312], [335, 336], [335, 339], [339, 340], [339, 343], [343, 344], [343, 350], [350, 351], [350, 358], [358, 359], [358, 362], [362, 363], [362, 366], [366, 367], [366, 370], [370, 371]], "missing_branches": [[122, 124], [183, 185], [370, 381]]}, "": {"executed_lines": [1, 8, 9, 11, 12, 15, 16, 22, 44, 86, 128, 148, 189, 208, 234, 253, 272, 314, 325, 389, 392, 401, 404, 413, 416, 425], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/formatters/pretty_formatter.py": {"executed_lines": [1, 8, 9, 10, 12, 14, 15, 18, 19, 25, 34, 35, 38, 39, 40, 41, 43, 55, 56, 57, 59, 62, 63, 64, 66, 68, 69, 72, 73, 74, 77, 78, 79, 82, 85, 89, 90, 94, 95, 99, 100, 105, 106, 107, 109, 112, 113, 114, 115, 117, 118, 121, 122, 123, 128, 130, 132, 133, 137, 149, 150, 151, 153, 156, 157, 158, 160, 161, 163, 164, 165, 167, 169, 170, 175, 187, 188, 189, 191, 192, 194, 195, 197, 198, 200, 201, 203, 204, 207, 208, 209, 210, 213, 214, 216, 217, 221, 233, 234, 235, 238, 239, 242, 243, 244, 245, 246, 247, 249, 256, 268, 269, 270, 272, 273, 275, 276, 278, 279, 289, 301, 302, 305, 306, 307, 310, 311, 314, 315, 318, 319, 320, 322, 329, 340, 341, 343, 345, 355, 356, 358, 367, 369, 380, 381, 382, 384, 385, 387, 396, 402, 404, 414, 415, 416, 417, 419, 421, 430, 431, 433, 436, 437, 440, 441, 442, 443, 445, 446, 449, 450, 451, 452, 455, 456, 458, 460, 470, 473, 474, 475, 476, 477, 479, 483, 484, 486, 488, 491, 492, 493, 496, 497, 498, 500, 502, 511, 512, 513, 515, 516, 517, 518, 519, 521, 524, 525, 531, 532, 533, 535, 537, 539, 548, 549, 551, 552, 554, 555, 556, 557, 558, 560, 561, 563, 572, 576, 577, 578, 580, 583, 584, 585, 586, 587, 589, 590, 593, 594, 595, 597, 600, 601, 602, 603, 604, 605, 606, 609, 610, 613, 614, 615, 616, 617, 618, 621, 622, 623, 624, 625, 626, 627, 629, 631, 632, 633, 635, 637, 646, 647, 648, 652, 654, 657, 659, 660, 661, 663, 664, 665, 667, 668, 669, 671, 675, 678, 687, 690, 699, 702, 711], "summary": {"covered_lines": 290, "num_statements": 299, "percent_covered": 96.01873536299766, "percent_covered_display": "96.02", "missing_lines": 9, "excluded_lines": 0, "num_branches": 128, "num_partial_branches": 8, "covered_branches": 120, "missing_branches": 8}, "missing_lines": [212, 251, 252, 281, 283, 284, 324, 325, 573], "excluded_lines": [], "executed_branches": [[56, 57], [56, 59], [66, 68], [66, 128], [78, 79], [78, 82], [105, 106], [105, 121], [112, 113], [112, 121], [117, 112], [117, 118], [121, 66], [121, 122], [150, 151], [150, 153], [160, 161], [160, 167], [163, 160], [163, 164], [188, 189], [188, 191], [191, 192], [191, 194], [194, 195], [194, 197], [197, 198], [197, 200], [200, 201], [200, 203], [203, 204], [203, 207], [209, 210], [234, 235], [234, 238], [238, 239], [238, 242], [243, 244], [243, 247], [269, 270], [269, 272], [272, 273], [272, 275], [275, 276], [275, 278], [278, 279], [310, 311], [314, 315], [318, 319], [318, 322], [319, 318], [319, 320], [340, 341], [340, 343], [381, 382], [381, 384], [414, 415], [414, 416], [416, 417], [416, 419], [430, 431], [430, 433], [440, 441], [441, 442], [441, 445], [449, 450], [449, 455], [455, 456], [455, 458], [474, 475], [474, 479], [483, 484], [483, 486], [491, 492], [496, 497], [496, 500], [515, 516], [515, 537], [516, 517], [516, 519], [519, 521], [519, 524], [524, 525], [524, 535], [531, 515], [531, 532], [532, 533], [548, 549], [548, 551], [554, 555], [554, 560], [572, 576], [577, 578], [577, 580], [584, 585], [584, 593], [586, 584], [586, 587], [594, 595], [594, 597], [601, 602], [601, 606], [613, 614], [613, 635], [615, 616], [615, 633], [617, 618], [617, 621], [621, 622], [621, 623], [623, 624], [623, 626], [626, 627], [626, 629], [659, 660], [659, 663], [663, 664], [663, 667], [667, 668], [667, 671]], "missing_branches": [[209, 212], [278, 281], [310, 314], [314, 318], [440, 449], [491, 496], [532, 531], [572, 573]], "functions": {"PrettyFormatter.__init__": {"executed_lines": [34, 35, 38, 39, 40, 41], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter.format_threads": {"executed_lines": [55, 56, 57, 59, 62, 63, 64, 66, 68, 69, 72, 73, 74, 77, 78, 79, 82, 85, 89, 90, 94, 95, 99, 100, 105, 106, 107, 109, 112, 113, 114, 115, 117, 118, 121, 122, 123, 128, 130, 132, 133], "summary": {"covered_lines": 41, "num_statements": 41, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 0, "covered_branches": 14, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[56, 57], [56, 59], [66, 68], [66, 128], [78, 79], [78, 82], [105, 106], [105, 121], [112, 113], [112, 121], [117, 112], [117, 118], [121, 66], [121, 122]], "missing_branches": []}, "PrettyFormatter.format_comments": {"executed_lines": [149, 150, 151, 153, 156, 157, 158, 160, 161, 163, 164, 165, 167, 169, 170], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[150, 151], [150, 153], [160, 161], [160, 167], [163, 160], [163, 164]], "missing_branches": []}, "PrettyFormatter.format_object": {"executed_lines": [187, 188, 189, 191, 192, 194, 195, 197, 198, 200, 201, 203, 204, 207, 208, 209, 210, 213, 214, 216, 217], "summary": {"covered_lines": 21, "num_statements": 22, "percent_covered": 94.44444444444444, "percent_covered_display": "94.44", "missing_lines": 1, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 1, "covered_branches": 13, "missing_branches": 1}, "missing_lines": [212], "excluded_lines": [], "executed_branches": [[188, 189], [188, 191], [191, 192], [191, 194], [194, 195], [194, 197], [197, 198], [197, 200], [200, 201], [200, 203], [203, 204], [203, 207], [209, 210]], "missing_branches": [[209, 212]]}, "PrettyFormatter.format_array": {"executed_lines": [233, 234, 235, 238, 239, 242, 243, 244, 245, 246, 247, 249], "summary": {"covered_lines": 12, "num_statements": 14, "percent_covered": 90.0, "percent_covered_display": "90.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [251, 252], "excluded_lines": [], "executed_branches": [[234, 235], [234, 238], [238, 239], [238, 242], [243, 244], [243, 247]], "missing_branches": []}, "PrettyFormatter.format_primitive": {"executed_lines": [268, 269, 270, 272, 273, 275, 276, 278, 279], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 80.0, "percent_covered_display": "80.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [281, 283, 284], "excluded_lines": [], "executed_branches": [[269, 270], [269, 272], [272, 273], [272, 275], [275, 276], [275, 278], [278, 279]], "missing_branches": [[278, 281]]}, "PrettyFormatter.format_error": {"executed_lines": [301, 302, 305, 306, 307, 310, 311, 314, 315, 318, 319, 320, 322], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 82.6086956521739, "percent_covered_display": "82.61", "missing_lines": 2, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2}, "missing_lines": [324, 325], "excluded_lines": [], "executed_branches": [[310, 311], [314, 315], [318, 319], [318, 322], [319, 318], [319, 320]], "missing_branches": [[310, 314], [314, 318]]}, "PrettyFormatter._style": {"executed_lines": [340, 341, 343], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[340, 341], [340, 343]], "missing_branches": []}, "PrettyFormatter._strip_ansi_codes": {"executed_lines": [355, 356], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._display_width": {"executed_lines": [367], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._pad_to_width": {"executed_lines": [380, 381, 382, 384, 385], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[381, 382], [381, 384]], "missing_branches": []}, "PrettyFormatter._get_status_color": {"executed_lines": [396, 402], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PrettyFormatter._get_status_emoji": {"executed_lines": [414, 415, 416, 417, 419], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[414, 415], [414, 416], [416, 417], [416, 419]], "missing_branches": []}, "PrettyFormatter._format_file_context": {"executed_lines": [430, 431, 433, 436, 437, 440, 441, 442, 443, 445, 446, 449, 450, 451, 452, 455, 456, 458], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.42857142857143, "percent_covered_display": "96.43", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[430, 431], [430, 433], [440, 441], [441, 442], [441, 445], [449, 450], [449, 455], [455, 456], [455, 458]], "missing_branches": [[440, 449]]}, "PrettyFormatter._format_comment": {"executed_lines": [470, 473, 474, 475, 476, 477, 479, 483, 484, 486, 488, 491, 492, 493, 496, 497, 498, 500], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 96.15384615384616, "percent_covered_display": "96.15", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[474, 475], [474, 479], [483, 484], [483, 486], [491, 492], [496, 497], [496, 500]], "missing_branches": [[491, 496]]}, "PrettyFormatter._wrap_comment_content": {"executed_lines": [511, 512, 513, 515, 516, 517, 518, 519, 521, 524, 525, 531, 532, 533, 535, 537], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 96.42857142857143, "percent_covered_display": "96.43", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 1, "covered_branches": 11, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[515, 516], [515, 537], [516, 517], [516, 519], [519, 521], [519, 524], [524, 525], [524, 535], [531, 515], [531, 532], [532, 533]], "missing_branches": [[532, 531]]}, "PrettyFormatter._format_dict": {"executed_lines": [548, 549, 551, 552, 554, 555, 556, 557, 558, 560, 561], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[548, 549], [548, 551], [554, 555], [554, 560]], "missing_branches": []}, "PrettyFormatter._format_table": {"executed_lines": [572, 576, 577, 578, 580, 583, 584, 585, 586, 587, 589, 590, 593, 594, 595, 597, 600, 601, 602, 603, 604, 605, 606, 609, 610, 613, 614, 615, 616, 617, 618, 621, 622, 623, 624, 625, 626, 627, 629, 631, 632, 633, 635], "summary": {"covered_lines": 43, "num_statements": 44, "percent_covered": 97.05882352941177, "percent_covered_display": "97.06", "missing_lines": 1, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [573], "excluded_lines": [], "executed_branches": [[572, 576], [577, 578], [577, 580], [584, 585], [584, 593], [586, 584], [586, 587], [594, 595], [594, 597], [601, 602], [601, 606], [613, 614], [613, 635], [615, 616], [615, 633], [617, 618], [617, 621], [621, 622], [621, 623], [623, 624], [623, 626], [626, 627], [626, 629]], "missing_branches": [[572, 573]]}, "PrettyFormatter._format_summary": {"executed_lines": [646, 647, 648, 652, 654, 657, 659, 660, 661, 663, 664, 665, 667, 668, 669, 671], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[659, 660], [659, 663], [663, 664], [663, 667], [667, 668], [667, 671]], "missing_branches": []}, "format_threads_pretty": {"executed_lines": [687], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_comments_pretty": {"executed_lines": [699], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "format_object_pretty": {"executed_lines": [711], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 10, 12, 14, 15, 18, 19, 25, 43, 137, 175, 221, 256, 289, 329, 345, 358, 369, 387, 404, 421, 460, 502, 539, 563, 637, 675, 678, 690, 702], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"PrettyFormatter": {"executed_lines": [34, 35, 38, 39, 40, 41, 55, 56, 57, 59, 62, 63, 64, 66, 68, 69, 72, 73, 74, 77, 78, 79, 82, 85, 89, 90, 94, 95, 99, 100, 105, 106, 107, 109, 112, 113, 114, 115, 117, 118, 121, 122, 123, 128, 130, 132, 133, 149, 150, 151, 153, 156, 157, 158, 160, 161, 163, 164, 165, 167, 169, 170, 187, 188, 189, 191, 192, 194, 195, 197, 198, 200, 201, 203, 204, 207, 208, 209, 210, 213, 214, 216, 217, 233, 234, 235, 238, 239, 242, 243, 244, 245, 246, 247, 249, 268, 269, 270, 272, 273, 275, 276, 278, 279, 301, 302, 305, 306, 307, 310, 311, 314, 315, 318, 319, 320, 322, 340, 341, 343, 355, 356, 367, 380, 381, 382, 384, 385, 396, 402, 414, 415, 416, 417, 419, 430, 431, 433, 436, 437, 440, 441, 442, 443, 445, 446, 449, 450, 451, 452, 455, 456, 458, 470, 473, 474, 475, 476, 477, 479, 483, 484, 486, 488, 491, 492, 493, 496, 497, 498, 500, 511, 512, 513, 515, 516, 517, 518, 519, 521, 524, 525, 531, 532, 533, 535, 537, 548, 549, 551, 552, 554, 555, 556, 557, 558, 560, 561, 572, 576, 577, 578, 580, 583, 584, 585, 586, 587, 589, 590, 593, 594, 595, 597, 600, 601, 602, 603, 604, 605, 606, 609, 610, 613, 614, 615, 616, 617, 618, 621, 622, 623, 624, 625, 626, 627, 629, 631, 632, 633, 635, 646, 647, 648, 652, 654, 657, 659, 660, 661, 663, 664, 665, 667, 668, 669, 671], "summary": {"covered_lines": 257, "num_statements": 266, "percent_covered": 95.68527918781726, "percent_covered_display": "95.69", "missing_lines": 9, "excluded_lines": 0, "num_branches": 128, "num_partial_branches": 8, "covered_branches": 120, "missing_branches": 8}, "missing_lines": [212, 251, 252, 281, 283, 284, 324, 325, 573], "excluded_lines": [], "executed_branches": [[56, 57], [56, 59], [66, 68], [66, 128], [78, 79], [78, 82], [105, 106], [105, 121], [112, 113], [112, 121], [117, 112], [117, 118], [121, 66], [121, 122], [150, 151], [150, 153], [160, 161], [160, 167], [163, 160], [163, 164], [188, 189], [188, 191], [191, 192], [191, 194], [194, 195], [194, 197], [197, 198], [197, 200], [200, 201], [200, 203], [203, 204], [203, 207], [209, 210], [234, 235], [234, 238], [238, 239], [238, 242], [243, 244], [243, 247], [269, 270], [269, 272], [272, 273], [272, 275], [275, 276], [275, 278], [278, 279], [310, 311], [314, 315], [318, 319], [318, 322], [319, 318], [319, 320], [340, 341], [340, 343], [381, 382], [381, 384], [414, 415], [414, 416], [416, 417], [416, 419], [430, 431], [430, 433], [440, 441], [441, 442], [441, 445], [449, 450], [449, 455], [455, 456], [455, 458], [474, 475], [474, 479], [483, 484], [483, 486], [491, 492], [496, 497], [496, 500], [515, 516], [515, 537], [516, 517], [516, 519], [519, 521], [519, 524], [524, 525], [524, 535], [531, 515], [531, 532], [532, 533], [548, 549], [548, 551], [554, 555], [554, 560], [572, 576], [577, 578], [577, 580], [584, 585], [584, 593], [586, 584], [586, 587], [594, 595], [594, 597], [601, 602], [601, 606], [613, 614], [613, 635], [615, 616], [615, 633], [617, 618], [617, 621], [621, 622], [621, 623], [623, 624], [623, 626], [626, 627], [626, 629], [659, 660], [659, 663], [663, 664], [663, 667], [667, 668], [667, 671]], "missing_branches": [[209, 212], [278, 281], [310, 314], [314, 318], [440, 449], [491, 496], [532, 531], [572, 573]]}, "": {"executed_lines": [1, 8, 9, 10, 12, 14, 15, 18, 19, 25, 43, 137, 175, 221, 256, 289, 329, 345, 358, 369, 387, 404, 421, 460, 502, 539, 563, 637, 675, 678, 687, 690, 699, 702, 711], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/models/__init__.py": {"executed_lines": [1, 3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1, 3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/models/models.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 11, 23, 24, 25, 27, 29, 30, 38, 39, 40, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 75, 77, 83, 85, 86, 92, 93, 101, 102, 108, 109, 117, 118, 124, 125, 136, 143, 144, 152, 159, 166, 167, 175, 184, 186, 188, 198, 205, 207, 224, 225, 238, 246, 247, 248, 256, 257, 258, 259, 266, 267, 268, 269, 277, 278, 279, 280, 283, 286, 303, 304, 310, 312, 314, 320, 321, 322, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 351, 353, 359, 361, 368, 369, 377, 384, 385, 391, 392, 404, 411, 412, 420, 427, 428, 436, 443, 450, 451, 458, 460, 470, 479, 480, 501, 502, 514, 516, 525, 533, 534, 535, 547, 548, 549, 550, 557, 558, 559, 560, 568, 569, 593, 595, 604, 606, 612, 613, 614, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 642, 648, 650, 651, 657, 658, 666, 673, 674, 682, 689, 690, 698, 705, 706, 714, 721, 722, 730, 731, 739, 746, 755, 762, 763, 771, 778, 785, 786, 793, 795, 805, 811, 825, 826, 839, 851, 852, 853, 861, 862, 863, 864, 871, 872, 882, 896, 898, 899], "summary": {"covered_lines": 205, "num_statements": 241, "percent_covered": 83.48082595870207, "percent_covered_display": "83.48", "missing_lines": 36, "excluded_lines": 0, "num_branches": 98, "num_partial_branches": 20, "covered_branches": 78, "missing_branches": 20}, "missing_lines": [137, 153, 160, 176, 189, 191, 362, 378, 405, 421, 437, 444, 461, 463, 493, 494, 517, 582, 584, 585, 586, 596, 597, 667, 683, 699, 715, 740, 747, 756, 772, 779, 796, 798, 873, 874], "excluded_lines": [], "executed_branches": [[27, 29], [27, 30], [85, 86], [85, 92], [92, 93], [92, 101], [101, 102], [101, 108], [108, 109], [108, 117], [117, 118], [117, 124], [124, 125], [124, 136], [136, 143], [143, 144], [143, 152], [152, 159], [159, 166], [166, 167], [166, 175], [175, 184], [247, 248], [247, 256], [278, 279], [278, 286], [279, 280], [279, 283], [361, 368], [368, 369], [368, 377], [377, 384], [384, 385], [384, 391], [391, 392], [391, 404], [404, 411], [411, 412], [411, 420], [420, 427], [427, 428], [427, 436], [436, 443], [443, 450], [450, -353], [450, 451], [516, 525], [534, 535], [534, 547], [650, 651], [650, 657], [657, 658], [657, 666], [666, 673], [673, 674], [673, 682], [682, 689], [689, 690], [689, 698], [698, 705], [705, 706], [705, 714], [714, 721], [721, 722], [721, 730], [730, 731], [730, 739], [739, 746], [746, 755], [755, 762], [762, 763], [762, 771], [771, 778], [778, 785], [785, -642], [785, 786], [852, 853], [852, 861]], "missing_branches": [[136, 137], [152, 153], [159, 160], [175, 176], [361, 362], [377, 378], [404, 405], [420, 421], [436, 437], [443, 444], [516, 517], [666, 667], [682, 683], [698, 699], [714, 715], [739, 740], [746, 747], [755, 756], [771, 772], [778, 779]], "functions": {"_parse_datetime": {"executed_lines": [23, 24, 25, 27, 29, 30], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[27, 29], [27, 30]], "missing_branches": []}, "ReviewThread.__post_init__": {"executed_lines": [83, 85, 86, 92, 93, 101, 102, 108, 109, 117, 118, 124, 125, 136, 143, 144, 152, 159, 166, 167, 175, 184, 186, 188], "summary": {"covered_lines": 24, "num_statements": 30, "percent_covered": 81.48148148148148, "percent_covered_display": "81.48", "missing_lines": 6, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 4, "covered_branches": 20, "missing_branches": 4}, "missing_lines": [137, 153, 160, 176, 189, 191], "excluded_lines": [], "executed_branches": [[85, 86], [85, 92], [92, 93], [92, 101], [101, 102], [101, 108], [108, 109], [108, 117], [117, 118], [117, 124], [124, 125], [124, 136], [136, 143], [143, 144], [143, 152], [152, 159], [159, 166], [166, 167], [166, 175], [175, 184]], "missing_branches": [[136, 137], [152, 153], [159, 160], [175, 176]]}, "ReviewThread.to_dict": {"executed_lines": [205, 207], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThread.from_dict": {"executed_lines": [238, 246, 247, 248, 256, 257, 258, 259, 266, 267, 268, 269, 277, 278, 279, 280, 283, 286], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[247, 248], [247, 256], [278, 279], [278, 286], [279, 280], [279, 283]], "missing_branches": []}, "ReviewThread.is_resolved": {"executed_lines": [310], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThread.__str__": {"executed_lines": [314], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "Comment.__post_init__": {"executed_lines": [359, 361, 368, 369, 377, 384, 385, 391, 392, 404, 411, 412, 420, 427, 428, 436, 443, 450, 451, 458, 460], "summary": {"covered_lines": 21, "num_statements": 29, "percent_covered": 73.58490566037736, "percent_covered_display": "73.58", "missing_lines": 8, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 6, "covered_branches": 18, "missing_branches": 6}, "missing_lines": [362, 378, 405, 421, 437, 444, 461, 463], "excluded_lines": [], "executed_branches": [[361, 368], [368, 369], [368, 377], [377, 384], [384, 385], [384, 391], [391, 392], [391, 404], [404, 411], [411, 412], [411, 420], [420, 427], [427, 428], [427, 436], [436, 443], [443, 450], [450, -353], [450, 451]], "missing_branches": [[361, 362], [377, 378], [404, 405], [420, 421], [436, 437], [443, 444]]}, "Comment.to_dict": {"executed_lines": [479, 480], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [493, 494], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "Comment.from_dict": {"executed_lines": [514, 516, 525, 533, 534, 535, 547, 548, 549, 550, 557, 558, 559, 560, 568, 569, 593, 595], "summary": {"covered_lines": 18, "num_statements": 25, "percent_covered": 72.41379310344827, "percent_covered_display": "72.41", "missing_lines": 7, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [517, 582, 584, 585, 586, 596, 597], "excluded_lines": [], "executed_branches": [[516, 525], [534, 535], [534, 547]], "missing_branches": [[516, 517]]}, "Comment.__str__": {"executed_lines": [606], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequest.__post_init__": {"executed_lines": [648, 650, 651, 657, 658, 666, 673, 674, 682, 689, 690, 698, 705, 706, 714, 721, 722, 730, 731, 739, 746, 755, 762, 763, 771, 778, 785, 786, 793, 795], "summary": {"covered_lines": 30, "num_statements": 41, "percent_covered": 74.02597402597402, "percent_covered_display": "74.03", "missing_lines": 11, "excluded_lines": 0, "num_branches": 36, "num_partial_branches": 9, "covered_branches": 27, "missing_branches": 9}, "missing_lines": [667, 683, 699, 715, 740, 747, 756, 772, 779, 796, 798], "excluded_lines": [], "executed_branches": [[650, 651], [650, 657], [657, 658], [657, 666], [666, 673], [673, 674], [673, 682], [682, 689], [689, 690], [689, 698], [698, 705], [705, 706], [705, 714], [714, 721], [721, 722], [721, 730], [730, 731], [730, 739], [739, 746], [746, 755], [755, 762], [762, 763], [762, 771], [771, 778], [778, 785], [785, -642], [785, 786]], "missing_branches": [[666, 667], [682, 683], [698, 699], [714, 715], [739, 740], [746, 747], [755, 756], [771, 772], [778, 779]]}, "PullRequest.to_dict": {"executed_lines": [811], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequest.from_dict": {"executed_lines": [839, 851, 852, 853, 861, 862, 863, 864, 871, 872, 882], "summary": {"covered_lines": 11, "num_statements": 13, "percent_covered": 86.66666666666667, "percent_covered_display": "86.67", "missing_lines": 2, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [873, 874], "excluded_lines": [], "executed_branches": [[852, 853], [852, 861]], "missing_branches": []}, "PullRequest.__str__": {"executed_lines": [898, 899], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 11, 38, 39, 40, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 75, 77, 198, 224, 225, 303, 304, 312, 320, 321, 322, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 351, 353, 470, 501, 502, 604, 612, 613, 614, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 642, 805, 825, 826, 896], "summary": {"covered_lines": 67, "num_statements": 67, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ReviewThread": {"executed_lines": [83, 85, 86, 92, 93, 101, 102, 108, 109, 117, 118, 124, 125, 136, 143, 144, 152, 159, 166, 167, 175, 184, 186, 188, 205, 207, 238, 246, 247, 248, 256, 257, 258, 259, 266, 267, 268, 269, 277, 278, 279, 280, 283, 286, 310, 314], "summary": {"covered_lines": 46, "num_statements": 52, "percent_covered": 87.8048780487805, "percent_covered_display": "87.80", "missing_lines": 6, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 4, "covered_branches": 26, "missing_branches": 4}, "missing_lines": [137, 153, 160, 176, 189, 191], "excluded_lines": [], "executed_branches": [[85, 86], [85, 92], [92, 93], [92, 101], [101, 102], [101, 108], [108, 109], [108, 117], [117, 118], [117, 124], [124, 125], [124, 136], [136, 143], [143, 144], [143, 152], [152, 159], [159, 166], [166, 167], [166, 175], [175, 184], [247, 248], [247, 256], [278, 279], [278, 286], [279, 280], [279, 283]], "missing_branches": [[136, 137], [152, 153], [159, 160], [175, 176]]}, "Comment": {"executed_lines": [359, 361, 368, 369, 377, 384, 385, 391, 392, 404, 411, 412, 420, 427, 428, 436, 443, 450, 451, 458, 460, 479, 480, 514, 516, 525, 533, 534, 535, 547, 548, 549, 550, 557, 558, 559, 560, 568, 569, 593, 595, 606], "summary": {"covered_lines": 42, "num_statements": 59, "percent_covered": 72.41379310344827, "percent_covered_display": "72.41", "missing_lines": 17, "excluded_lines": 0, "num_branches": 28, "num_partial_branches": 7, "covered_branches": 21, "missing_branches": 7}, "missing_lines": [362, 378, 405, 421, 437, 444, 461, 463, 493, 494, 517, 582, 584, 585, 586, 596, 597], "excluded_lines": [], "executed_branches": [[361, 368], [368, 369], [368, 377], [377, 384], [384, 385], [384, 391], [391, 392], [391, 404], [404, 411], [411, 412], [411, 420], [420, 427], [427, 428], [427, 436], [436, 443], [443, 450], [450, -353], [450, 451], [516, 525], [534, 535], [534, 547]], "missing_branches": [[361, 362], [377, 378], [404, 405], [420, 421], [436, 437], [443, 444], [516, 517]]}, "PullRequest": {"executed_lines": [648, 650, 651, 657, 658, 666, 673, 674, 682, 689, 690, 698, 705, 706, 714, 721, 722, 730, 731, 739, 746, 755, 762, 763, 771, 778, 785, 786, 793, 795, 811, 839, 851, 852, 853, 861, 862, 863, 864, 871, 872, 882, 898, 899], "summary": {"covered_lines": 44, "num_statements": 57, "percent_covered": 76.84210526315789, "percent_covered_display": "76.84", "missing_lines": 13, "excluded_lines": 0, "num_branches": 38, "num_partial_branches": 9, "covered_branches": 29, "missing_branches": 9}, "missing_lines": [667, 683, 699, 715, 740, 747, 756, 772, 779, 796, 798, 873, 874], "excluded_lines": [], "executed_branches": [[650, 651], [650, 657], [657, 658], [657, 666], [666, 673], [673, 674], [673, 682], [682, 689], [689, 690], [689, 698], [698, 705], [705, 706], [705, 714], [714, 721], [721, 722], [721, 730], [730, 731], [730, 739], [739, 746], [746, 755], [755, 762], [762, 763], [762, 771], [771, 778], [778, 785], [785, -642], [785, 786], [852, 853], [852, 861]], "missing_branches": [[666, 667], [682, 683], [698, 699], [714, 715], [739, 740], [746, 747], [755, 756], [771, 772], [778, 779]]}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 11, 23, 24, 25, 27, 29, 30, 38, 39, 40, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 75, 77, 198, 224, 225, 303, 304, 312, 320, 321, 322, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 351, 353, 470, 501, 502, 604, 612, 613, 614, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 642, 805, 825, 826, 896], "summary": {"covered_lines": 73, "num_statements": 73, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[27, 29], [27, 30]], "missing_branches": []}}}, "src/toady/parsers/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/parsers/graphql_parser.py": {"executed_lines": [1, 7, 8, 9, 12, 13, 14, 16, 17, 18, 19, 20, 23, 24, 25, 27, 28, 29, 30, 33, 34, 36, 38, 40, 53, 56, 58, 60, 63, 66, 67, 69, 71, 74, 80, 81, 83, 86, 87, 90, 91, 92, 95, 96, 98, 99, 101, 104, 106, 113, 115, 118, 121, 122, 123, 124, 125, 127, 129, 133, 134, 136, 138, 142, 144, 145, 148, 149, 150, 152, 154, 156, 161, 164, 168, 169, 172, 174, 175, 178, 179, 180, 181, 184, 185, 186, 189, 190, 191, 194, 195, 197, 199, 200, 202, 205, 206, 207, 209, 217, 219, 224, 226, 227, 239, 242, 243, 245, 249, 250, 254, 262, 264, 266, 269, 272, 273, 274, 275, 279, 281, 282, 284, 285, 287, 290, 292, 294, 296, 298, 301, 302, 303, 305, 306, 308, 312, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 328, 330, 339, 341, 342, 343, 344, 345, 347, 348, 350, 359, 361, 364, 365, 368, 370, 371, 373, 374], "summary": {"covered_lines": 164, "num_statements": 181, "percent_covered": 87.93774319066148, "percent_covered_display": "87.94", "missing_lines": 17, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 10, "covered_branches": 62, "missing_branches": 14}, "missing_lines": [162, 165, 176, 203, 229, 230, 231, 233, 234, 235, 237, 246, 299, 309, 310, 313, 314], "excluded_lines": [], "executed_branches": [[80, 81], [80, 83], [91, 92], [91, 95], [98, 99], [98, 101], [122, 123], [122, 127], [136, 138], [136, 154], [138, 142], [138, 144], [144, 145], [144, 148], [149, 150], [161, 164], [164, 168], [168, 169], [168, 172], [175, 178], [185, 186], [185, 189], [190, 191], [190, 194], [197, 199], [197, 209], [202, 205], [227, 239], [245, 249], [273, 274], [273, 294], [279, 281], [279, 282], [282, 284], [282, 285], [285, 287], [285, 290], [298, 301], [305, 306], [305, 328], [308, 312], [312, 316], [316, 317], [316, 318], [318, 319], [318, 320], [320, 305], [320, 321], [321, 322], [321, 323], [323, 305], [323, 324], [325, 305], [325, 326], [342, -341], [342, 343], [344, 342], [344, 345], [364, -361], [364, 365], [370, 364], [370, 371]], "missing_branches": [[149, 152], [161, 162], [164, 165], [175, 176], [202, 203], [227, 229], [230, 231], [230, 233], [234, 235], [234, 237], [245, 246], [298, 299], [308, 309], [312, 313]], "functions": {"GraphQLParser.__init__": {"executed_lines": [38], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser.parse": {"executed_lines": [53, 56, 58], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser._clean_query": {"executed_lines": [63, 66, 67, 69], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser._parse_operation": {"executed_lines": [74, 80, 81, 83, 86, 87, 90, 91, 92, 95, 96, 98, 99, 101, 104, 106], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[80, 81], [80, 83], [91, 92], [91, 95], [98, 99], [98, 101]], "missing_branches": []}, "GraphQLParser._parse_variables": {"executed_lines": [115, 118, 121, 122, 123, 124, 125, 127], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[122, 123], [122, 127]], "missing_branches": []}, "GraphQLParser._parse_selections": {"executed_lines": [133, 134, 136, 138, 142, 144, 145, 148, 149, 150, 152, 154], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 95.0, "percent_covered_display": "95.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[136, 138], [136, 154], [138, 142], [138, 144], [144, 145], [144, 148], [149, 150]], "missing_branches": [[149, 152]]}, "GraphQLParser._parse_field": {"executed_lines": [161, 164, 168, 169, 172, 174, 175, 178, 179, 180, 181, 184, 185, 186, 189, 190, 191, 194, 195, 197, 199, 200, 202, 205, 206, 207, 209, 217], "summary": {"covered_lines": 28, "num_statements": 32, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 4, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 4, "covered_branches": 12, "missing_branches": 4}, "missing_lines": [162, 165, 176, 203], "excluded_lines": [], "executed_branches": [[161, 164], [164, 168], [168, 169], [168, 172], [175, 178], [185, 186], [185, 189], [190, 191], [190, 194], [197, 199], [197, 209], [202, 205]], "missing_branches": [[161, 162], [164, 165], [175, 176], [202, 203]]}, "GraphQLParser._parse_inline_fragment": {"executed_lines": [224, 226, 227, 239, 242, 243, 245, 249, 250, 254, 262], "summary": {"covered_lines": 11, "num_statements": 19, "percent_covered": 48.148148148148145, "percent_covered_display": "48.15", "missing_lines": 8, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 2, "missing_branches": 6}, "missing_lines": [229, 230, 231, 233, 234, 235, 237, 246], "excluded_lines": [], "executed_branches": [[227, 239], [245, 249]], "missing_branches": [[227, 229], [230, 231], [230, 233], [234, 235], [234, 237], [245, 246]]}, "GraphQLParser._parse_arguments": {"executed_lines": [266, 269, 272, 273, 274, 275, 279, 281, 282, 284, 285, 287, 290, 292, 294], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[273, 274], [273, 294], [279, 281], [279, 282], [282, 284], [282, 285], [285, 287], [285, 290]], "missing_branches": []}, "GraphQLParser._find_matching_brace": {"executed_lines": [298, 301, 302, 303, 305, 306, 308, 312, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 328], "summary": {"covered_lines": 20, "num_statements": 25, "percent_covered": 82.22222222222223, "percent_covered_display": "82.22", "missing_lines": 5, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [299, 309, 310, 313, 314], "excluded_lines": [], "executed_branches": [[298, 301], [305, 306], [305, 328], [308, 312], [312, 316], [316, 317], [316, 318], [318, 319], [318, 320], [320, 305], [320, 321], [321, 322], [321, 323], [323, 305], [323, 324], [325, 305], [325, 326]], "missing_branches": [[298, 299], [308, 309], [312, 313]]}, "GraphQLParser.extract_all_fields": {"executed_lines": [339, 341, 347, 348], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser.extract_all_fields.collect_fields": {"executed_lines": [342, 343, 344, 345], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[342, -341], [342, 343], [344, 342], [344, 345]], "missing_branches": []}, "GraphQLParser.extract_field_paths": {"executed_lines": [359, 361, 373, 374], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser.extract_field_paths.collect_paths": {"executed_lines": [364, 365, 368, 370, 371], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[364, -361], [364, 365], [370, 364], [370, 371]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 12, 13, 14, 16, 17, 18, 19, 20, 23, 24, 25, 27, 28, 29, 30, 33, 34, 36, 40, 60, 71, 113, 129, 156, 219, 264, 296, 330, 350], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GraphQLField": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLOperation": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLParser": {"executed_lines": [38, 53, 56, 58, 63, 66, 67, 69, 74, 80, 81, 83, 86, 87, 90, 91, 92, 95, 96, 98, 99, 101, 104, 106, 115, 118, 121, 122, 123, 124, 125, 127, 133, 134, 136, 138, 142, 144, 145, 148, 149, 150, 152, 154, 161, 164, 168, 169, 172, 174, 175, 178, 179, 180, 181, 184, 185, 186, 189, 190, 191, 194, 195, 197, 199, 200, 202, 205, 206, 207, 209, 217, 224, 226, 227, 239, 242, 243, 245, 249, 250, 254, 262, 266, 269, 272, 273, 274, 275, 279, 281, 282, 284, 285, 287, 290, 292, 294, 298, 301, 302, 303, 305, 306, 308, 312, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 328, 339, 341, 342, 343, 344, 345, 347, 348, 359, 361, 364, 365, 368, 370, 371, 373, 374], "summary": {"covered_lines": 135, "num_statements": 152, "percent_covered": 86.40350877192982, "percent_covered_display": "86.40", "missing_lines": 17, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 10, "covered_branches": 62, "missing_branches": 14}, "missing_lines": [162, 165, 176, 203, 229, 230, 231, 233, 234, 235, 237, 246, 299, 309, 310, 313, 314], "excluded_lines": [], "executed_branches": [[80, 81], [80, 83], [91, 92], [91, 95], [98, 99], [98, 101], [122, 123], [122, 127], [136, 138], [136, 154], [138, 142], [138, 144], [144, 145], [144, 148], [149, 150], [161, 164], [164, 168], [168, 169], [168, 172], [175, 178], [185, 186], [185, 189], [190, 191], [190, 194], [197, 199], [197, 209], [202, 205], [227, 239], [245, 249], [273, 274], [273, 294], [279, 281], [279, 282], [282, 284], [282, 285], [285, 287], [285, 290], [298, 301], [305, 306], [305, 328], [308, 312], [312, 316], [316, 317], [316, 318], [318, 319], [318, 320], [320, 305], [320, 321], [321, 322], [321, 323], [323, 305], [323, 324], [325, 305], [325, 326], [342, -341], [342, 343], [344, 342], [344, 345], [364, -361], [364, 365], [370, 364], [370, 371]], "missing_branches": [[149, 152], [161, 162], [164, 165], [175, 176], [202, 203], [227, 229], [230, 231], [230, 233], [234, 235], [234, 237], [245, 246], [298, 299], [308, 309], [312, 313]]}, "": {"executed_lines": [1, 7, 8, 9, 12, 13, 14, 16, 17, 18, 19, 20, 23, 24, 25, 27, 28, 29, 30, 33, 34, 36, 40, 60, 71, 113, 129, 156, 219, 264, 296, 330, 350], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/parsers/graphql_queries.py": {"executed_lines": [1, 3, 4, 5, 8, 23, 24, 27, 28, 32, 33, 38, 39, 40, 41, 43, 46, 47, 49, 51, 52, 53, 55, 64, 65, 67, 79, 80, 81, 82, 84, 96, 97, 98, 99, 101, 108, 164, 166, 177, 179, 185, 188, 201, 202, 203, 204, 205, 208, 222, 223, 226, 227, 229, 276, 279, 297, 298, 300, 302, 303, 305, 308, 309, 311, 313, 314, 315, 317, 329, 330, 331, 332, 334, 343, 344, 346, 352, 389, 391, 401, 403, 409, 412, 424, 425, 426, 427], "summary": {"covered_lines": 85, "num_statements": 85, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 18, "num_partial_branches": 0, "covered_branches": 18, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[23, 24], [23, 27], [27, 28], [27, 32], [32, 33], [32, 38], [79, 80], [79, 81], [96, 97], [96, 98], [222, 223], [222, 226], [297, 298], [297, 300], [302, 303], [302, 305], [329, 330], [329, 331]], "missing_branches": [], "functions": {"_validate_cursor": {"executed_lines": [23, 24, 27, 28, 32, 33, 38, 39, 40, 41, 43], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[23, 24], [23, 27], [27, 28], [27, 32], [32, 33], [32, 38]], "missing_branches": []}, "ReviewThreadQueryBuilder.__init__": {"executed_lines": [51, 52, 53], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.include_resolved": {"executed_lines": [64, 65], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.limit": {"executed_lines": [79, 80, 81, 82], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[79, 80], [79, 81]], "missing_branches": []}, "ReviewThreadQueryBuilder.comment_limit": {"executed_lines": [96, 97, 98, 99], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[96, 97], [96, 98]], "missing_branches": []}, "ReviewThreadQueryBuilder.build_query": {"executed_lines": [108, 164], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.build_variables": {"executed_lines": [177], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReviewThreadQueryBuilder.should_filter_resolved": {"executed_lines": [185], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "build_review_threads_query": {"executed_lines": [201, 202, 203, 204, 205], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_paginated_query": {"executed_lines": [222, 223, 226, 227, 229, 276], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[222, 223], [222, 226]], "missing_branches": []}, "create_paginated_query_variables": {"executed_lines": [297, 298, 300, 302, 303, 305], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[297, 298], [297, 300], [302, 303], [302, 305]], "missing_branches": []}, "PullRequestQueryBuilder.__init__": {"executed_lines": [313, 314, 315], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.limit": {"executed_lines": [329, 330, 331, 332], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[329, 330], [329, 331]], "missing_branches": []}, "PullRequestQueryBuilder.include_drafts": {"executed_lines": [343, 344], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.build_query": {"executed_lines": [352, 389], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.build_variables": {"executed_lines": [401], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PullRequestQueryBuilder.should_filter_drafts": {"executed_lines": [409], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "build_open_prs_query": {"executed_lines": [424, 425, 426, 427], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 8, 46, 47, 49, 55, 67, 84, 101, 166, 179, 188, 208, 279, 308, 309, 311, 317, 334, 346, 391, 403, 412], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ReviewThreadQueryBuilder": {"executed_lines": [51, 52, 53, 64, 65, 79, 80, 81, 82, 96, 97, 98, 99, 108, 164, 177, 185], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[79, 80], [79, 81], [96, 97], [96, 98]], "missing_branches": []}, "PullRequestQueryBuilder": {"executed_lines": [313, 314, 315, 329, 330, 331, 332, 343, 344, 352, 389, 401, 409], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[329, 330], [329, 331]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 8, 23, 24, 27, 28, 32, 33, 38, 39, 40, 41, 43, 46, 47, 49, 55, 67, 84, 101, 166, 179, 188, 201, 202, 203, 204, 205, 208, 222, 223, 226, 227, 229, 276, 279, 297, 298, 300, 302, 303, 305, 308, 309, 311, 317, 334, 346, 391, 403, 412, 424, 425, 426, 427], "summary": {"covered_lines": 55, "num_statements": 55, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[23, 24], [23, 27], [27, 28], [27, 32], [32, 33], [32, 38], [222, 223], [222, 226], [297, 298], [297, 300], [302, 303], [302, 305]], "missing_branches": []}}}, "src/toady/parsers/parsers.py": {"executed_lines": [1, 3, 5, 10, 11, 14, 15, 17, 19, 21, 36, 38, 41, 42, 44, 45, 52, 53, 54, 55, 56, 57, 59, 66, 68, 75, 83, 95, 97, 100, 102, 105, 106, 107, 115, 116, 117, 119, 120, 121, 135, 136, 137, 138, 139, 140, 141, 154, 155, 156, 157, 158, 159, 160, 162, 179, 181, 197, 212, 214, 215, 216, 223, 226, 227, 236, 237, 247, 248, 249, 252, 253, 254, 255, 256, 262, 263, 264, 265, 275, 277, 291, 293, 312, 321, 322, 325, 326, 327, 328, 330, 345, 347, 350, 353, 355, 363, 364, 366, 386, 401, 403, 406, 407, 409, 418, 419, 420, 422, 425, 426, 428, 430, 437, 439, 441, 442, 449, 457, 469, 471, 472, 477, 478, 481, 482, 483, 484, 491, 492, 502, 516, 518, 535, 536, 538, 539, 552, 553, 560, 561, 562, 563, 564, 568, 573, 579, 586, 587, 596, 604, 605, 606, 613, 614, 621, 622, 623, 630, 638, 640, 641, 654, 662, 663, 664, 665, 666, 670, 675, 688, 689, 698, 706, 707, 708, 715, 716, 723, 725, 726, 738, 739, 746, 755, 756, 757, 764, 766, 767, 779, 787, 788, 789, 790, 798, 799, 800, 807, 808, 815, 817, 818, 830, 838, 839, 840, 841, 848, 850, 851], "summary": {"covered_lines": 208, "num_statements": 280, "percent_covered": 73.42105263157895, "percent_covered_display": "73.42", "missing_lines": 72, "excluded_lines": 0, "num_branches": 100, "num_partial_branches": 13, "covered_branches": 71, "missing_branches": 29}, "missing_lines": [69, 76, 122, 124, 142, 143, 182, 183, 189, 190, 228, 229, 238, 239, 257, 259, 266, 267, 268, 269, 271, 272, 294, 295, 304, 305, 356, 368, 370, 371, 372, 378, 379, 410, 443, 450, 493, 494, 519, 520, 526, 527, 588, 597, 631, 655, 681, 690, 699, 780, 801, 831, 864, 865, 872, 873, 874, 875, 876, 880, 884, 891, 892, 893, 901, 902, 909, 910, 911, 918, 919, 926], "excluded_lines": [], "executed_branches": [[44, 45], [44, 52], [53, 54], [53, 66], [106, 107], [106, 115], [116, 117], [116, 135], [215, 216], [215, 223], [254, 255], [254, 262], [265, 275], [321, 322], [321, 325], [326, 327], [326, 328], [355, 363], [409, 418], [419, 420], [419, 437], [552, 553], [552, 560], [560, 561], [560, 586], [561, 562], [561, 579], [563, 564], [563, 573], [587, 596], [596, 604], [605, 606], [605, 613], [613, 614], [613, 621], [622, 623], [622, 630], [630, 638], [654, 662], [662, 663], [662, 688], [663, 664], [665, 666], [665, 675], [689, 698], [698, 706], [707, 708], [707, 715], [715, 716], [715, 723], [738, 739], [738, 746], [755, 756], [755, 764], [756, 755], [756, 757], [779, 787], [788, 789], [788, 798], [789, 788], [789, 790], [798, 799], [798, 815], [800, 807], [807, 808], [807, 815], [830, 838], [839, 840], [839, 848], [840, 839], [840, 841]], "missing_branches": [[265, 266], [355, 356], [409, 410], [587, 588], [596, 597], [630, 631], [654, 655], [663, 681], [689, 690], [698, 699], [779, 780], [800, 801], [830, 831], [864, 865], [864, 872], [872, 873], [872, 891], [873, 874], [873, 884], [875, 876], [875, 884], [892, 893], [892, 901], [901, 902], [901, 909], [910, 911], [910, 918], [918, 919], [918, 926]], "functions": {"GraphQLResponseParser.__init__": {"executed_lines": [19], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GraphQLResponseParser.parse_review_threads_response": {"executed_lines": [36, 38, 41, 42, 44, 45, 52, 53, 54, 55, 56, 57, 59, 66, 68, 75], "summary": {"covered_lines": 16, "num_statements": 18, "percent_covered": 90.9090909090909, "percent_covered_display": "90.91", "missing_lines": 2, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [69, 76], "excluded_lines": [], "executed_branches": [[44, 45], [44, 52], [53, 54], [53, 66]], "missing_branches": []}, "GraphQLResponseParser._parse_single_review_thread": {"executed_lines": [95, 97, 100, 102, 105, 106, 107, 115, 116, 117, 119, 120, 121, 135, 136, 137, 138, 139, 140, 141, 154, 155, 156, 157, 158, 159, 160, 162, 179, 181], "summary": {"covered_lines": 30, "num_statements": 38, "percent_covered": 80.95238095238095, "percent_covered_display": "80.95", "missing_lines": 8, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [122, 124, 142, 143, 182, 183, 189, 190], "excluded_lines": [], "executed_branches": [[106, 107], [106, 115], [116, 117], [116, 135]], "missing_branches": []}, "GraphQLResponseParser._parse_single_comment": {"executed_lines": [212, 214, 215, 216, 223, 226, 227, 236, 237, 247, 248, 249, 252, 253, 254, 255, 256, 262, 263, 264, 265, 275, 277, 291, 293], "summary": {"covered_lines": 25, "num_statements": 41, "percent_covered": 63.829787234042556, "percent_covered_display": "63.83", "missing_lines": 16, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [228, 229, 238, 239, 257, 259, 266, 267, 268, 269, 271, 272, 294, 295, 304, 305], "excluded_lines": [], "executed_branches": [[215, 216], [215, 223], [254, 255], [254, 262], [265, 275]], "missing_branches": [[265, 266]]}, "GraphQLResponseParser._extract_title_from_comment": {"executed_lines": [321, 322, 325, 326, 327, 328], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[321, 322], [321, 325], [326, 327], [326, 328]], "missing_branches": []}, "GraphQLResponseParser.parse_paginated_response": {"executed_lines": [345, 347, 350, 353, 355, 363, 364, 366], "summary": {"covered_lines": 8, "num_statements": 15, "percent_covered": 52.94117647058823, "percent_covered_display": "52.94", "missing_lines": 7, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [356, 368, 370, 371, 372, 378, 379], "excluded_lines": [], "executed_branches": [[355, 363]], "missing_branches": [[355, 356]]}, "GraphQLResponseParser.parse_pull_requests_response": {"executed_lines": [401, 403, 406, 407, 409, 418, 419, 420, 422, 425, 426, 428, 430, 437, 439, 441, 442, 449], "summary": {"covered_lines": 18, "num_statements": 21, "percent_covered": 84.0, "percent_covered_display": "84.00", "missing_lines": 3, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [410, 443, 450], "excluded_lines": [], "executed_branches": [[409, 418], [419, 420], [419, 437]], "missing_branches": [[409, 410]]}, "GraphQLResponseParser._parse_pull_request_data": {"executed_lines": [469, 471, 472, 477, 478, 481, 482, 483, 484, 491, 492, 502, 516, 518], "summary": {"covered_lines": 14, "num_statements": 20, "percent_covered": 70.0, "percent_covered_display": "70.00", "missing_lines": 6, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [493, 494, 519, 520, 526, 527], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ResponseValidator.validate_graphql_response": {"executed_lines": [552, 553, 560, 561, 562, 563, 564, 568, 573, 579, 586, 587, 596, 604, 605, 606, 613, 614, 621, 622, 623, 630, 638], "summary": {"covered_lines": 23, "num_statements": 26, "percent_covered": 86.95652173913044, "percent_covered_display": "86.96", "missing_lines": 3, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [588, 597, 631], "excluded_lines": [], "executed_branches": [[552, 553], [552, 560], [560, 561], [560, 586], [561, 562], [561, 579], [563, 564], [563, 573], [587, 596], [596, 604], [605, 606], [605, 613], [613, 614], [613, 621], [622, 623], [622, 630], [630, 638]], "missing_branches": [[587, 588], [596, 597], [630, 631]]}, "ResponseValidator.validate_graphql_prs_response": {"executed_lines": [654, 662, 663, 664, 665, 666, 670, 675, 688, 689, 698, 706, 707, 708, 715, 716, 723], "summary": {"covered_lines": 17, "num_statements": 21, "percent_covered": 78.37837837837837, "percent_covered_display": "78.38", "missing_lines": 4, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 4, "covered_branches": 12, "missing_branches": 4}, "missing_lines": [655, 681, 690, 699], "excluded_lines": [], "executed_branches": [[654, 662], [662, 663], [662, 688], [663, 664], [665, 666], [665, 675], [689, 698], [698, 706], [707, 708], [707, 715], [715, 716], [715, 723]], "missing_branches": [[654, 655], [663, 681], [689, 690], [698, 699]]}, "ResponseValidator.validate_pull_request_data": {"executed_lines": [738, 739, 746, 755, 756, 757, 764], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[738, 739], [738, 746], [755, 756], [755, 764], [756, 755], [756, 757]], "missing_branches": []}, "ResponseValidator.validate_review_thread_data": {"executed_lines": [779, 787, 788, 789, 790, 798, 799, 800, 807, 808, 815], "summary": {"covered_lines": 11, "num_statements": 13, "percent_covered": 84.0, "percent_covered_display": "84.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 2, "covered_branches": 10, "missing_branches": 2}, "missing_lines": [780, 801], "excluded_lines": [], "executed_branches": [[779, 787], [788, 789], [788, 798], [789, 788], [789, 790], [798, 799], [798, 815], [800, 807], [807, 808], [807, 815]], "missing_branches": [[779, 780], [800, 801]]}, "ResponseValidator.validate_comment_data": {"executed_lines": [830, 838, 839, 840, 841, 848], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 84.61538461538461, "percent_covered_display": "84.62", "missing_lines": 1, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [831], "excluded_lines": [], "executed_branches": [[830, 838], [839, 840], [839, 848], [840, 839], [840, 841]], "missing_branches": [[830, 831]]}, "ResponseValidator.validate_pull_requests_response": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 20, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 20, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 16}, "missing_lines": [864, 865, 872, 873, 874, 875, 876, 880, 884, 891, 892, 893, 901, 902, 909, 910, 911, 918, 919, 926], "excluded_lines": [], "executed_branches": [], "missing_branches": [[864, 865], [864, 872], [872, 873], [872, 891], [873, 874], [873, 884], [875, 876], [875, 884], [892, 893], [892, 901], [901, 902], [901, 909], [910, 911], [910, 918], [918, 919], [918, 926]]}, "": {"executed_lines": [1, 3, 5, 10, 11, 14, 15, 17, 21, 83, 197, 312, 330, 386, 457, 535, 536, 538, 539, 640, 641, 725, 726, 766, 767, 817, 818, 850, 851], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GraphQLResponseParser": {"executed_lines": [19, 36, 38, 41, 42, 44, 45, 52, 53, 54, 55, 56, 57, 59, 66, 68, 75, 95, 97, 100, 102, 105, 106, 107, 115, 116, 117, 119, 120, 121, 135, 136, 137, 138, 139, 140, 141, 154, 155, 156, 157, 158, 159, 160, 162, 179, 181, 212, 214, 215, 216, 223, 226, 227, 236, 237, 247, 248, 249, 252, 253, 254, 255, 256, 262, 263, 264, 265, 275, 277, 291, 293, 321, 322, 325, 326, 327, 328, 345, 347, 350, 353, 355, 363, 364, 366, 401, 403, 406, 407, 409, 418, 419, 420, 422, 425, 426, 428, 430, 437, 439, 441, 442, 449, 469, 471, 472, 477, 478, 481, 482, 483, 484, 491, 492, 502, 516, 518], "summary": {"covered_lines": 118, "num_statements": 160, "percent_covered": 75.54347826086956, "percent_covered_display": "75.54", "missing_lines": 42, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 3, "covered_branches": 21, "missing_branches": 3}, "missing_lines": [69, 76, 122, 124, 142, 143, 182, 183, 189, 190, 228, 229, 238, 239, 257, 259, 266, 267, 268, 269, 271, 272, 294, 295, 304, 305, 356, 368, 370, 371, 372, 378, 379, 410, 443, 450, 493, 494, 519, 520, 526, 527], "excluded_lines": [], "executed_branches": [[44, 45], [44, 52], [53, 54], [53, 66], [106, 107], [106, 115], [116, 117], [116, 135], [215, 216], [215, 223], [254, 255], [254, 262], [265, 275], [321, 322], [321, 325], [326, 327], [326, 328], [355, 363], [409, 418], [419, 420], [419, 437]], "missing_branches": [[265, 266], [355, 356], [409, 410]]}, "ResponseValidator": {"executed_lines": [552, 553, 560, 561, 562, 563, 564, 568, 573, 579, 586, 587, 596, 604, 605, 606, 613, 614, 621, 622, 623, 630, 638, 654, 662, 663, 664, 665, 666, 670, 675, 688, 689, 698, 706, 707, 708, 715, 716, 723, 738, 739, 746, 755, 756, 757, 764, 779, 787, 788, 789, 790, 798, 799, 800, 807, 808, 815, 830, 838, 839, 840, 841, 848], "summary": {"covered_lines": 64, "num_statements": 94, "percent_covered": 67.05882352941177, "percent_covered_display": "67.06", "missing_lines": 30, "excluded_lines": 0, "num_branches": 76, "num_partial_branches": 10, "covered_branches": 50, "missing_branches": 26}, "missing_lines": [588, 597, 631, 655, 681, 690, 699, 780, 801, 831, 864, 865, 872, 873, 874, 875, 876, 880, 884, 891, 892, 893, 901, 902, 909, 910, 911, 918, 919, 926], "excluded_lines": [], "executed_branches": [[552, 553], [552, 560], [560, 561], [560, 586], [561, 562], [561, 579], [563, 564], [563, 573], [587, 596], [596, 604], [605, 606], [605, 613], [613, 614], [613, 621], [622, 623], [622, 630], [630, 638], [654, 662], [662, 663], [662, 688], [663, 664], [665, 666], [665, 675], [689, 698], [698, 706], [707, 708], [707, 715], [715, 716], [715, 723], [738, 739], [738, 746], [755, 756], [755, 764], [756, 755], [756, 757], [779, 787], [788, 789], [788, 798], [789, 788], [789, 790], [798, 799], [798, 815], [800, 807], [807, 808], [807, 815], [830, 838], [839, 840], [839, 848], [840, 839], [840, 841]], "missing_branches": [[587, 588], [596, 597], [630, 631], [654, 655], [663, 681], [689, 690], [698, 699], [779, 780], [800, 801], [830, 831], [864, 865], [864, 872], [872, 873], [872, 891], [873, 874], [873, 884], [875, 876], [875, 884], [892, 893], [892, 901], [901, 902], [901, 909], [910, 911], [910, 918], [918, 919], [918, 926]]}, "": {"executed_lines": [1, 3, 5, 10, 11, 14, 15, 17, 21, 83, 197, 312, 330, 386, 457, 535, 536, 538, 539, 640, 641, 725, 726, 766, 767, 817, 818, 850, 851], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/fetch_service.py": {"executed_lines": [1, 3, 5, 6, 10, 11, 12, 15, 16, 18, 21, 22, 24, 35, 36, 37, 43, 44, 45, 47, 72, 74, 79, 80, 83, 86, 89, 90, 93, 95, 97, 98, 100, 102, 111, 112, 113, 118, 119, 120, 122, 124, 146, 149, 157, 180, 182, 185, 186, 189, 192, 195, 196, 199, 201, 203, 204, 206, 208, 228, 231, 238, 263, 265, 271, 273, 274, 276, 278, 279, 280, 284, 285, 286, 287, 289, 291, 292, 294, 296, 325, 327, 329, 332, 337, 339, 342, 344, 347, 353, 355, 357, 358, 360], "summary": {"covered_lines": 91, "num_statements": 93, "percent_covered": 96.7479674796748, "percent_covered_display": "96.75", "missing_lines": 2, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 2, "covered_branches": 28, "missing_branches": 2}, "missing_lines": [38, 343], "excluded_lines": [], "executed_branches": [[37, 43], [89, 90], [89, 93], [97, 98], [97, 100], [112, 113], [112, 118], [119, 120], [119, 122], [195, 196], [195, 199], [203, 204], [203, 206], [271, 273], [271, 276], [276, 278], [276, 284], [285, 286], [285, 287], [291, 292], [291, 294], [327, 329], [327, 332], [337, 339], [337, 342], [342, 344], [357, 358], [357, 360]], "missing_branches": [[37, 38], [342, 343]], "functions": {"FetchService.__init__": {"executed_lines": [35, 36, 37, 43, 44, 45], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 77.77777777777777, "percent_covered_display": "77.78", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [38], "excluded_lines": [], "executed_branches": [[37, 43]], "missing_branches": [[37, 38]]}, "FetchService.fetch_review_threads": {"executed_lines": [72, 74, 79, 80, 83, 86, 89, 90, 93, 95, 97, 98, 100], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[89, 90], [89, 93], [97, 98], [97, 100]], "missing_branches": []}, "FetchService._get_repository_info": {"executed_lines": [111, 112, 113, 118, 119, 120, 122], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[112, 113], [112, 118], [119, 120], [119, 122]], "missing_branches": []}, "FetchService.fetch_review_threads_from_current_repo": {"executed_lines": [146, 149], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FetchService.fetch_open_pull_requests": {"executed_lines": [180, 182, 185, 186, 189, 192, 195, 196, 199, 201, 203, 204, 206], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[195, 196], [195, 199], [203, 204], [203, 206]], "missing_branches": []}, "FetchService.fetch_open_pull_requests_from_current_repo": {"executed_lines": [228, 231], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FetchService.select_pr_interactively": {"executed_lines": [263, 265, 271, 273, 274, 276, 278, 279, 280, 284, 285, 286, 287, 289, 291, 292, 294], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[271, 273], [271, 276], [276, 278], [276, 284], [285, 286], [285, 287], [291, 292], [291, 294]], "missing_branches": []}, "FetchService.fetch_review_threads_with_pr_selection": {"executed_lines": [325, 327, 329, 332, 337, 339, 342, 344, 347, 353, 355, 357, 358, 360], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 91.30434782608695, "percent_covered_display": "91.30", "missing_lines": 1, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [343], "excluded_lines": [], "executed_branches": [[327, 329], [327, 332], [337, 339], [337, 342], [342, 344], [357, 358], [357, 360]], "missing_branches": [[342, 343]]}, "": {"executed_lines": [1, 3, 5, 6, 10, 11, 12, 15, 16, 18, 21, 22, 24, 47, 102, 124, 157, 208, 238, 296], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"FetchServiceError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "FetchService": {"executed_lines": [35, 36, 37, 43, 44, 45, 72, 74, 79, 80, 83, 86, 89, 90, 93, 95, 97, 98, 100, 111, 112, 113, 118, 119, 120, 122, 146, 149, 180, 182, 185, 186, 189, 192, 195, 196, 199, 201, 203, 204, 206, 228, 231, 263, 265, 271, 273, 274, 276, 278, 279, 280, 284, 285, 286, 287, 289, 291, 292, 294, 325, 327, 329, 332, 337, 339, 342, 344, 347, 353, 355, 357, 358, 360], "summary": {"covered_lines": 74, "num_statements": 76, "percent_covered": 96.22641509433963, "percent_covered_display": "96.23", "missing_lines": 2, "excluded_lines": 0, "num_branches": 30, "num_partial_branches": 2, "covered_branches": 28, "missing_branches": 2}, "missing_lines": [38, 343], "excluded_lines": [], "executed_branches": [[37, 43], [89, 90], [89, 93], [97, 98], [97, 100], [112, 113], [112, 118], [119, 120], [119, 122], [195, 196], [195, 199], [203, 204], [203, 206], [271, 273], [271, 276], [276, 278], [276, 284], [285, 286], [285, 287], [291, 292], [291, 294], [327, 329], [327, 332], [337, 339], [337, 342], [342, 344], [357, 358], [357, 360]], "missing_branches": [[37, 38], [342, 343]]}, "": {"executed_lines": [1, 3, 5, 6, 10, 11, 12, 15, 16, 18, 21, 22, 24, 47, 102, 124, 157, 208, 238, 296], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/github_service.py": {"executed_lines": [1, 3, 4, 5, 8, 34, 63, 80, 98, 99, 101, 104, 105, 107, 110, 111, 113, 116, 117, 119, 122, 123, 125, 128, 129, 131, 134, 135, 137, 146, 147, 149, 150, 152, 158, 159, 165, 166, 167, 169, 178, 179, 185, 186, 191, 192, 193, 195, 196, 197, 201, 207, 208, 214, 215, 216, 218, 230, 231, 232, 235, 236, 239, 240, 241, 243, 245, 262, 263, 265, 267, 268, 277, 278, 283, 287, 292, 296, 301, 302, 304, 306, 307, 310, 311, 315, 329, 331, 332, 333, 334, 336, 346, 347, 348, 349, 350, 351, 352, 354, 373, 377, 378, 379, 381, 383, 384, 387, 388, 391, 395, 396, 397, 399, 411, 414, 419, 420, 421, 422, 424, 425, 427, 429, 443, 444, 445, 446, 448, 449, 451, 452, 454, 456, 467, 468, 479, 480, 481, 483, 500, 501, 502, 503, 505, 506, 509, 511, 513, 515, 516, 518, 519, 522, 524, 527, 528, 530, 531, 536, 537, 539, 567, 591, 645], "summary": {"covered_lines": 157, "num_statements": 214, "percent_covered": 70.50359712230215, "percent_covered_display": "70.50", "missing_lines": 57, "excluded_lines": 0, "num_branches": 64, "num_partial_branches": 1, "covered_branches": 39, "missing_branches": 25}, "missing_lines": [553, 554, 556, 559, 561, 563, 564, 565, 577, 578, 583, 584, 585, 586, 589, 605, 606, 618, 619, 621, 622, 625, 629, 630, 631, 633, 634, 635, 637, 639, 641, 642, 643, 666, 667, 668, 669, 670, 671, 674, 677, 678, 679, 682, 685, 686, 687, 688, 690, 691, 694, 695, 697, 699, 700, 703, 704], "excluded_lines": [], "executed_branches": [[146, 147], [146, 149], [185, 186], [185, 191], [191, 192], [191, 195], [192, 191], [192, 193], [231, 232], [231, 235], [262, 263], [262, 265], [277, 278], [277, 283], [283, 287], [283, 292], [292, 296], [292, 301], [301, 302], [301, 304], [377, 378], [377, 381], [378, 379], [378, 381], [387, 388], [387, 395], [419, 420], [419, 427], [421, 419], [421, 422], [500, 501], [500, 502], [502, 503], [502, 505], [511, 513], [511, 522], [527, 528], [530, 531], [530, 536]], "missing_branches": [[527, 530], [553, 554], [553, 556], [577, 578], [577, 583], [583, 584], [583, 585], [585, 586], [585, 589], [621, 622], [621, 629], [630, 631], [630, 633], [634, 635], [634, 637], [666, 667], [666, 668], [668, 669], [668, 670], [670, 671], [670, 674], [687, 688], [687, 690], [694, 695], [694, 697]], "functions": {"GitHubService.__init__": {"executed_lines": [146, 147, 149, 150], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[146, 147], [146, 149]], "missing_branches": []}, "GitHubService.check_gh_installation": {"executed_lines": [158, 159, 165, 166, 167], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.get_gh_version": {"executed_lines": [178, 179, 185, 186, 191, 192, 193, 195, 196, 197], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[185, 186], [185, 191], [191, 192], [191, 195], [192, 191], [192, 193]], "missing_branches": []}, "GitHubService.check_authentication": {"executed_lines": [207, 208, 214, 215, 216], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.validate_version_compatibility": {"executed_lines": [230, 231, 232, 235, 236, 239, 240, 241, 243], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[231, 232], [231, 235]], "missing_branches": []}, "GitHubService.run_gh_command": {"executed_lines": [262, 263, 265, 267, 268, 277, 278, 283, 287, 292, 296, 301, 302, 304, 306, 307, 310, 311], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[262, 263], [262, 265], [277, 278], [277, 283], [283, 287], [283, 292], [292, 296], [292, 301], [301, 302], [301, 304]], "missing_branches": []}, "GitHubService.get_json_output": {"executed_lines": [329, 331, 332, 333, 334], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.get_current_repo": {"executed_lines": [346, 347, 348, 349, 350, 351, 352], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.execute_graphql_query": {"executed_lines": [373, 377, 378, 379, 381, 383, 384, 387, 388, 391, 395, 396, 397], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[377, 378], [377, 381], [378, 379], [378, 381], [387, 388], [387, 395]], "missing_branches": []}, "GitHubService.get_repo_info_from_url": {"executed_lines": [411, 414, 419, 420, 421, 422, 424, 425, 427], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[419, 420], [419, 427], [421, 419], [421, 422]], "missing_branches": []}, "GitHubService.validate_repository_access": {"executed_lines": [443, 444, 445, 446, 448, 449, 451, 452, 454], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.check_pr_exists": {"executed_lines": [467, 468, 479, 480, 481], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService.post_reply": {"executed_lines": [500, 501, 502, 503, 505, 506, 509, 511, 513, 515, 516, 518, 519, 522, 524, 527, 528, 530, 531, 536, 537], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 96.7741935483871, "percent_covered_display": "96.77", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[500, 501], [500, 502], [502, 503], [502, 505], [511, 513], [511, 522], [527, 528], [530, 531], [530, 536]], "missing_branches": [[527, 530]]}, "GitHubService.resolve_thread": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 8, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [553, 554, 556, 559, 561, 563, 564, 565], "excluded_lines": [], "executed_branches": [], "missing_branches": [[553, 554], [553, 556]]}, "GitHubService._determine_reply_strategy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 7, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6}, "missing_lines": [577, 578, 583, 584, 585, 586, 589], "excluded_lines": [], "executed_branches": [], "missing_branches": [[577, 578], [577, 583], [583, 584], [583, 585], [585, 586], [585, 589]]}, "GitHubService._get_review_id_for_comment": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 18, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 6}, "missing_lines": [605, 606, 618, 619, 621, 622, 625, 629, 630, 631, 633, 634, 635, 637, 639, 641, 642, 643], "excluded_lines": [], "executed_branches": [], "missing_branches": [[621, 622], [621, 629], [630, 631], [630, 633], [634, 635], [634, 637]]}, "GitHubService.fetch_open_pull_requests": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 24, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 24, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 10}, "missing_lines": [666, 667, 668, 669, 670, 671, 674, 677, 678, 679, 682, 685, 686, 687, 688, 690, 691, 694, 695, 697, 699, 700, 703, 704], "excluded_lines": [], "executed_branches": [], "missing_branches": [[666, 667], [666, 668], [668, 669], [668, 670], [670, 671], [670, 674], [687, 688], [687, 690], [694, 695], [694, 697]]}, "": {"executed_lines": [1, 3, 4, 5, 8, 34, 63, 80, 98, 99, 101, 104, 105, 107, 110, 111, 113, 116, 117, 119, 122, 123, 125, 128, 129, 131, 134, 135, 137, 152, 169, 201, 218, 245, 315, 336, 354, 399, 429, 456, 483, 539, 567, 591, 645], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GitHubServiceError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubCLINotFoundError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAuthenticationError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubAPIError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubTimeoutError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubRateLimitError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubService": {"executed_lines": [146, 147, 149, 150, 158, 159, 165, 166, 167, 178, 179, 185, 186, 191, 192, 193, 195, 196, 197, 207, 208, 214, 215, 216, 230, 231, 232, 235, 236, 239, 240, 241, 243, 262, 263, 265, 267, 268, 277, 278, 283, 287, 292, 296, 301, 302, 304, 306, 307, 310, 311, 329, 331, 332, 333, 334, 346, 347, 348, 349, 350, 351, 352, 373, 377, 378, 379, 381, 383, 384, 387, 388, 391, 395, 396, 397, 411, 414, 419, 420, 421, 422, 424, 425, 427, 443, 444, 445, 446, 448, 449, 451, 452, 454, 467, 468, 479, 480, 481, 500, 501, 502, 503, 505, 506, 509, 511, 513, 515, 516, 518, 519, 522, 524, 527, 528, 530, 531, 536, 537], "summary": {"covered_lines": 120, "num_statements": 177, "percent_covered": 65.97510373443983, "percent_covered_display": "65.98", "missing_lines": 57, "excluded_lines": 0, "num_branches": 64, "num_partial_branches": 1, "covered_branches": 39, "missing_branches": 25}, "missing_lines": [553, 554, 556, 559, 561, 563, 564, 565, 577, 578, 583, 584, 585, 586, 589, 605, 606, 618, 619, 621, 622, 625, 629, 630, 631, 633, 634, 635, 637, 639, 641, 642, 643, 666, 667, 668, 669, 670, 671, 674, 677, 678, 679, 682, 685, 686, 687, 688, 690, 691, 694, 695, 697, 699, 700, 703, 704], "excluded_lines": [], "executed_branches": [[146, 147], [146, 149], [185, 186], [185, 191], [191, 192], [191, 195], [192, 191], [192, 193], [231, 232], [231, 235], [262, 263], [262, 265], [277, 278], [277, 283], [283, 287], [283, 292], [292, 296], [292, 301], [301, 302], [301, 304], [377, 378], [377, 381], [378, 379], [378, 381], [387, 388], [387, 395], [419, 420], [419, 427], [421, 419], [421, 422], [500, 501], [500, 502], [502, 503], [502, 505], [511, 513], [511, 522], [527, 528], [530, 531], [530, 536]], "missing_branches": [[527, 530], [553, 554], [553, 556], [577, 578], [577, 583], [583, 584], [583, 585], [585, 586], [585, 589], [621, 622], [621, 629], [630, 631], [630, 633], [634, 635], [634, 637], [666, 667], [666, 668], [668, 669], [668, 670], [670, 671], [670, 674], [687, 688], [687, 690], [694, 695], [694, 697]]}, "": {"executed_lines": [1, 3, 4, 5, 8, 34, 63, 80, 98, 99, 101, 104, 105, 107, 110, 111, 113, 116, 117, 119, 122, 123, 125, 128, 129, 131, 134, 135, 137, 152, 169, 201, 218, 245, 315, 336, 354, 399, 429, 456, 483, 539, 567, 591, 645], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/pr_selection.py": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 14, 17, 18, 20, 22, 24, 41, 42, 50, 51, 54, 55, 58, 59, 62, 67, 73, 78, 88, 92, 94, 107, 110, 111, 113, 115, 118, 119, 120, 123, 127, 132, 135, 136, 143, 144, 147, 148, 149, 150, 156, 157, 163, 164, 168, 170, 172, 173, 174, 175, 178, 193, 194, 201, 202, 210, 211, 212, 222], "summary": {"covered_lines": 63, "num_statements": 64, "percent_covered": 97.72727272727273, "percent_covered_display": "97.73", "missing_lines": 1, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [176], "excluded_lines": [], "executed_branches": [[41, 42], [41, 50], [50, 51], [50, 54], [54, 55], [54, 58], [58, 59], [58, 62], [113, 115], [113, 132], [119, 120], [119, 123], [143, 144], [143, 147], [156, 157], [156, 163], [174, 175], [193, 194], [193, 201], [201, 202], [201, 210], [211, 212], [211, 222]], "missing_branches": [[174, 176]], "functions": {"PRSelector.__init__": {"executed_lines": [22], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector.select_pull_request": {"executed_lines": [41, 42, 50, 51, 54, 55, 58, 59, 62], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[41, 42], [41, 50], [50, 51], [50, 54], [54, 55], [54, 58], [58, 59], [58, 62]], "missing_branches": []}, "PRSelector._handle_no_prs": {"executed_lines": [73], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector._handle_single_pr": {"executed_lines": [88, 92], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector._handle_multiple_prs": {"executed_lines": [107, 110, 111, 113, 115, 118, 119, 120, 123, 127, 132, 135, 136, 143, 144, 147, 148, 149, 150, 156, 157, 163, 164, 168, 170, 172, 173, 174, 175], "summary": {"covered_lines": 29, "num_statements": 30, "percent_covered": 95.0, "percent_covered_display": "95.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [176], "excluded_lines": [], "executed_branches": [[113, 115], [113, 132], [119, 120], [119, 123], [143, 144], [143, 147], [156, 157], [156, 163], [174, 175]], "missing_branches": [[174, 176]]}, "PRSelector.validate_pr_exists": {"executed_lines": [193, 194, 201, 202, 210, 211, 212, 222], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[193, 194], [193, 201], [201, 202], [201, 210], [211, 212], [211, 222]], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 14, 17, 18, 20, 24, 67, 78, 94, 178], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"PRSelectionError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector": {"executed_lines": [22, 41, 42, 50, 51, 54, 55, 58, 59, 62, 73, 88, 92, 107, 110, 111, 113, 115, 118, 119, 120, 123, 127, 132, 135, 136, 143, 144, 147, 148, 149, 150, 156, 157, 163, 164, 168, 170, 172, 173, 174, 175, 193, 194, 201, 202, 210, 211, 212, 222], "summary": {"covered_lines": 50, "num_statements": 51, "percent_covered": 97.33333333333333, "percent_covered_display": "97.33", "missing_lines": 1, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [176], "excluded_lines": [], "executed_branches": [[41, 42], [41, 50], [50, 51], [50, 54], [54, 55], [54, 58], [58, 59], [58, 62], [113, 115], [113, 132], [119, 120], [119, 123], [143, 144], [143, 147], [156, 157], [156, 163], [174, 175], [193, 194], [193, 201], [201, 202], [201, 210], [211, 212], [211, 222]], "missing_branches": [[174, 176]]}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 14, 17, 18, 20, 24, 67, 78, 94, 178], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/pr_selector.py": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 18, 25, 26, 27, 29, 41, 42, 44, 46, 49, 60, 62, 75, 76, 77, 78, 79, 82, 85, 87, 93, 95, 96, 97, 100, 101, 104, 105, 106, 109, 110, 111, 112, 117, 121, 125, 129, 131, 140, 141, 143, 144, 149, 152, 153, 154, 157, 158, 159, 160, 161, 165, 171, 174, 175, 176, 177, 183, 186, 189, 194, 195, 197, 199, 200, 201, 202, 204, 207, 211, 212, 213, 215, 218, 219, 221, 228, 232, 233, 234, 236, 237, 239, 240, 241, 244, 245, 247, 254, 255, 257, 258, 260, 262, 263, 265, 268, 274], "summary": {"covered_lines": 102, "num_statements": 106, "percent_covered": 94.44444444444444, "percent_covered_display": "94.44", "missing_lines": 4, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [52, 57, 208, 229], "excluded_lines": [], "executed_branches": [[41, 42], [41, 44], [44, 46], [44, 49], [49, 60], [93, -87], [93, 95], [105, 106], [105, 109], [110, 111], [110, 121], [152, 153], [152, 157], [174, 175], [174, 186], [207, 211], [228, 232]], "missing_branches": [[49, 52], [207, 208], [228, 229]], "functions": {"PRSelector.__init__": {"executed_lines": [25, 26, 27], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector.select_pr": {"executed_lines": [41, 42, 44, 46, 49, 60], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 78.57142857142857, "percent_covered_display": "78.57", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [52, 57], "excluded_lines": [], "executed_branches": [[41, 42], [41, 44], [44, 46], [44, 49], [49, 60]], "missing_branches": [[49, 52]]}, "PRSelector._show_pr_selection_menu": {"executed_lines": [75, 76, 77, 78, 79, 82, 85], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelector._display_pr_list": {"executed_lines": [93, 95, 96, 97, 100, 101, 104, 105, 106, 109, 110, 111, 112, 117, 121, 125, 129], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[93, -87], [93, 95], [105, 106], [105, 109], [110, 111], [110, 121]], "missing_branches": []}, "PRSelector._prompt_for_selection": {"executed_lines": [140, 141, 143, 144, 149, 152, 153, 154, 157, 158, 159, 160, 161, 165, 171, 174, 175, 176, 177, 183, 186, 189, 194, 195, 197, 199, 200, 201, 202], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[152, 153], [152, 157], [174, 175], [174, 186]], "missing_branches": []}, "PRSelector.display_no_prs_message": {"executed_lines": [207, 211, 212, 213, 215, 218, 219], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 80.0, "percent_covered_display": "80.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [208], "excluded_lines": [], "executed_branches": [[207, 211]], "missing_branches": [[207, 208]]}, "PRSelector.display_auto_selected_pr": {"executed_lines": [228, 232, 233, 234, 236, 237, 239, 240, 241], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [229], "excluded_lines": [], "executed_branches": [[228, 232]], "missing_branches": [[228, 229]]}, "PRSelectionResult.__init__": {"executed_lines": [254, 255], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelectionResult.has_selection": {"executed_lines": [260], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "PRSelectionResult.should_continue": {"executed_lines": [265], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_pr_selector": {"executed_lines": [274], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 18, 29, 62, 87, 131, 204, 221, 244, 245, 247, 257, 258, 262, 263, 268], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"PRSelector": {"executed_lines": [25, 26, 27, 41, 42, 44, 46, 49, 60, 75, 76, 77, 78, 79, 82, 85, 93, 95, 96, 97, 100, 101, 104, 105, 106, 109, 110, 111, 112, 117, 121, 125, 129, 140, 141, 143, 144, 149, 152, 153, 154, 157, 158, 159, 160, 161, 165, 171, 174, 175, 176, 177, 183, 186, 189, 194, 195, 197, 199, 200, 201, 202, 207, 211, 212, 213, 215, 218, 219, 228, 232, 233, 234, 236, 237, 239, 240, 241], "summary": {"covered_lines": 78, "num_statements": 82, "percent_covered": 93.13725490196079, "percent_covered_display": "93.14", "missing_lines": 4, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 3, "covered_branches": 17, "missing_branches": 3}, "missing_lines": [52, 57, 208, 229], "excluded_lines": [], "executed_branches": [[41, 42], [41, 44], [44, 46], [44, 49], [49, 60], [93, -87], [93, 95], [105, 106], [105, 109], [110, 111], [110, 121], [152, 153], [152, 157], [174, 175], [174, 186], [207, 211], [228, 232]], "missing_branches": [[49, 52], [207, 208], [228, 229]]}, "PRSelectionResult": {"executed_lines": [254, 255, 260, 265], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 5, 7, 8, 11, 12, 18, 29, 62, 87, 131, 204, 221, 244, 245, 247, 257, 258, 262, 263, 268, 274], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/reply_service.py": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 13, 16, 17, 19, 22, 23, 24, 26, 27, 28, 29, 32, 33, 35, 41, 43, 78, 79, 80, 81, 83, 84, 86, 88, 89, 92, 98, 105, 106, 110, 111, 112, 113, 114, 122, 123, 127, 131, 132, 133, 135, 136, 139, 141, 154, 155, 156, 159, 160, 162, 165, 166, 168, 188, 199, 205, 206, 207, 208, 210, 212, 229, 231, 232, 243, 244, 247, 258, 259, 262, 263, 266, 267, 269, 271, 272, 273, 275, 276, 288, 348, 357, 358, 359, 364, 365, 366, 368, 370, 384, 386, 387, 389, 390, 393, 394, 396, 397, 399, 412, 414, 415, 417, 418, 420, 423, 424, 425, 426, 429, 430, 431, 432, 435, 436, 439, 441, 442, 447, 448, 450, 456, 469, 470, 471, 473, 474, 476], "summary": {"covered_lines": 130, "num_statements": 162, "percent_covered": 73.66071428571429, "percent_covered_display": "73.66", "missing_lines": 32, "excluded_lines": 0, "num_branches": 62, "num_partial_branches": 11, "covered_branches": 35, "missing_branches": 27}, "missing_lines": [117, 118, 200, 201, 202, 280, 281, 286, 307, 308, 320, 321, 323, 324, 327, 328, 329, 333, 334, 335, 337, 338, 339, 341, 343, 344, 345, 346, 452, 454, 482, 483], "excluded_lines": [], "executed_branches": [[78, 79], [78, 83], [89, 92], [89, 98], [105, 106], [105, 110], [111, 112], [113, 114], [122, 123], [122, 127], [135, 136], [135, 139], [155, 156], [155, 165], [159, 160], [159, 162], [199, 205], [205, 206], [205, 210], [207, 208], [258, 259], [258, 262], [262, 263], [262, 269], [266, 267], [275, 276], [358, 359], [358, 364], [365, 366], [365, 368], [423, 424], [430, 431], [435, 436], [439, 441], [447, 448]], "missing_branches": [[111, 122], [113, 117], [117, 118], [117, 122], [199, 200], [201, 202], [201, 205], [207, 210], [266, 269], [275, 280], [280, 281], [280, 286], [323, 324], [323, 333], [327, 328], [327, 329], [334, 335], [334, 337], [338, 339], [338, 341], [344, 345], [344, 346], [423, 435], [430, 435], [435, 439], [439, 450], [447, 450]], "functions": {"ReplyService.__init__": {"executed_lines": [41], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyService.post_reply": {"executed_lines": [78, 79, 80, 81, 83, 84, 86, 88, 89, 92, 98, 105, 106, 110, 111, 112, 113, 114, 122, 123, 127, 131, 132, 133, 135, 136, 139], "summary": {"covered_lines": 27, "num_statements": 29, "percent_covered": 86.66666666666667, "percent_covered_display": "86.67", "missing_lines": 2, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 2, "covered_branches": 12, "missing_branches": 4}, "missing_lines": [117, 118], "excluded_lines": [], "executed_branches": [[78, 79], [78, 83], [89, 92], [89, 98], [105, 106], [105, 110], [111, 112], [113, 114], [122, 123], [122, 127], [135, 136], [135, 139]], "missing_branches": [[111, 122], [113, 117], [117, 118], [117, 122]]}, "ReplyService._handle_graphql_errors": {"executed_lines": [154, 155, 156, 159, 160, 162, 165, 166], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[155, 156], [155, 165], [159, 160], [159, 162]], "missing_branches": []}, "ReplyService._build_reply_info_from_graphql": {"executed_lines": [188, 199, 205, 206, 207, 208, 210], "summary": {"covered_lines": 7, "num_statements": 10, "percent_covered": 61.111111111111114, "percent_covered_display": "61.11", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 4}, "missing_lines": [200, 201, 202], "excluded_lines": [], "executed_branches": [[199, 205], [205, 206], [205, 210], [207, 208]], "missing_branches": [[199, 200], [201, 202], [201, 205], [207, 210]]}, "ReplyService._post_reply_fallback_rest": {"executed_lines": [229, 231, 232, 243, 244, 247, 258, 259, 262, 263, 266, 267, 269, 271, 272, 273, 275, 276], "summary": {"covered_lines": 18, "num_statements": 21, "percent_covered": 77.41935483870968, "percent_covered_display": "77.42", "missing_lines": 3, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 4}, "missing_lines": [280, 281, 286], "excluded_lines": [], "executed_branches": [[258, 259], [258, 262], [262, 263], [262, 269], [266, 267], [275, 276]], "missing_branches": [[266, 269], [275, 280], [280, 281], [280, 286]]}, "ReplyService._get_review_id_for_comment": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 20, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 20, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 10}, "missing_lines": [307, 308, 320, 321, 323, 324, 327, 328, 329, 333, 334, 335, 337, 338, 339, 341, 343, 344, 345, 346], "excluded_lines": [], "executed_branches": [], "missing_branches": [[323, 324], [323, 333], [327, 328], [327, 329], [334, 335], [334, 337], [338, 339], [338, 341], [344, 345], [344, 346]]}, "ReplyService._get_repository_info": {"executed_lines": [357, 358, 359, 364, 365, 366, 368], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[358, 359], [358, 364], [365, 366], [365, 368]], "missing_branches": []}, "ReplyService.validate_comment_exists": {"executed_lines": [384, 386, 387, 389, 390, 393, 394, 396, 397], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyService._get_parent_comment_info": {"executed_lines": [412, 414, 415, 417, 418, 420, 423, 424, 425, 426, 429, 430, 431, 432, 435, 436, 439, 441, 442, 447, 448, 450], "summary": {"covered_lines": 22, "num_statements": 24, "percent_covered": 79.41176470588235, "percent_covered_display": "79.41", "missing_lines": 2, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 5, "covered_branches": 5, "missing_branches": 5}, "missing_lines": [452, 454], "excluded_lines": [], "executed_branches": [[423, 424], [430, 431], [435, 436], [439, 441], [447, 448]], "missing_branches": [[423, 435], [430, 435], [435, 439], [439, 450], [447, 450]]}, "ReplyService._get_pr_info": {"executed_lines": [469, 470, 471, 473, 474, 476], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [482, 483], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 13, 16, 17, 19, 22, 23, 24, 26, 27, 28, 29, 32, 33, 35, 43, 141, 168, 212, 288, 348, 370, 399, 456], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ReplyServiceError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "CommentNotFoundError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ReplyService": {"executed_lines": [41, 78, 79, 80, 81, 83, 84, 86, 88, 89, 92, 98, 105, 106, 110, 111, 112, 113, 114, 122, 123, 127, 131, 132, 133, 135, 136, 139, 154, 155, 156, 159, 160, 162, 165, 166, 188, 199, 205, 206, 207, 208, 210, 229, 231, 232, 243, 244, 247, 258, 259, 262, 263, 266, 267, 269, 271, 272, 273, 275, 276, 357, 358, 359, 364, 365, 366, 368, 384, 386, 387, 389, 390, 393, 394, 396, 397, 412, 414, 415, 417, 418, 420, 423, 424, 425, 426, 429, 430, 431, 432, 435, 436, 439, 441, 442, 447, 448, 450, 469, 470, 471, 473, 474, 476], "summary": {"covered_lines": 105, "num_statements": 137, "percent_covered": 70.35175879396985, "percent_covered_display": "70.35", "missing_lines": 32, "excluded_lines": 0, "num_branches": 62, "num_partial_branches": 11, "covered_branches": 35, "missing_branches": 27}, "missing_lines": [117, 118, 200, 201, 202, 280, 281, 286, 307, 308, 320, 321, 323, 324, 327, 328, 329, 333, 334, 335, 337, 338, 339, 341, 343, 344, 345, 346, 452, 454, 482, 483], "excluded_lines": [], "executed_branches": [[78, 79], [78, 83], [89, 92], [89, 98], [105, 106], [105, 110], [111, 112], [113, 114], [122, 123], [122, 127], [135, 136], [135, 139], [155, 156], [155, 165], [159, 160], [159, 162], [199, 205], [205, 206], [205, 210], [207, 208], [258, 259], [258, 262], [262, 263], [262, 269], [266, 267], [275, 276], [358, 359], [358, 364], [365, 366], [365, 368], [423, 424], [430, 431], [435, 436], [439, 441], [447, 448]], "missing_branches": [[111, 122], [113, 117], [117, 118], [117, 122], [199, 200], [201, 202], [201, 205], [207, 210], [266, 269], [275, 280], [280, 281], [280, 286], [323, 324], [323, 333], [327, 328], [327, 329], [334, 335], [334, 337], [338, 339], [338, 341], [344, 345], [344, 346], [423, 435], [430, 435], [435, 439], [439, 450], [447, 450]]}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 13, 16, 17, 19, 22, 23, 24, 26, 27, 28, 29, 32, 33, 35, 43, 141, 168, 212, 288, 348, 370, 399, 456], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/services/resolve_service.py": {"executed_lines": [1, 3, 4, 6, 15, 16, 23, 24, 26, 32, 34, 50, 52, 53, 54, 55, 63, 64, 65, 68, 70, 71, 77, 78, 81, 82, 88, 89, 95, 97, 104, 120, 128, 136, 152, 154, 155, 165, 166, 167, 170, 172, 173, 179, 180, 183, 184, 190, 191, 197, 199, 206, 223, 231, 239, 254, 255, 264, 265, 266, 267, 274, 277, 279, 283, 287, 292, 302, 303, 305, 313, 316, 325, 327, 335, 356, 358, 360, 367, 374, 381, 391, 415, 422, 423, 424, 425, 433, 434, 435, 436, 437, 438, 442, 450, 459, 460, 462, 464, 467, 468, 475, 476, 482, 484, 485, 492, 493, 495, 496, 504, 506, 508, 509, 517, 530, 532, 556, 566, 571, 572, 573, 576, 577, 578, 579, 581, 582, 584, 590, 591, 597, 603, 612, 614, 615, 619, 624, 633, 635, 636, 637, 639, 643, 644, 647, 648], "summary": {"covered_lines": 145, "num_statements": 183, "percent_covered": 79.32489451476793, "percent_covered_display": "79.32", "missing_lines": 38, "excluded_lines": 0, "num_branches": 54, "num_partial_branches": 11, "covered_branches": 43, "missing_branches": 11}, "missing_lines": [69, 105, 129, 131, 156, 157, 171, 207, 232, 234, 256, 269, 272, 306, 308, 328, 330, 361, 368, 375, 382, 426, 427, 440, 451, 452, 456, 469, 473, 486, 490, 533, 534, 543, 599, 601, 620, 622], "excluded_lines": [], "executed_branches": [[77, 78], [77, 81], [88, 89], [88, 95], [179, 180], [179, 183], [190, 191], [190, 197], [255, 264], [265, 266], [265, 313], [267, 274], [279, 283], [279, 287], [287, 292], [287, 302], [360, 367], [367, 374], [374, 381], [381, 391], [433, 434], [433, 459], [436, 437], [436, 442], [437, 438], [462, 464], [462, 467], [468, 475], [475, 476], [475, 484], [485, 492], [495, 496], [495, 506], [572, 573], [572, 576], [577, 578], [581, 582], [584, 590], [584, 597], [614, 615], [614, 619], [636, 637], [636, 647]], "missing_branches": [[255, 256], [267, 269], [360, 361], [367, 368], [374, 375], [381, 382], [437, 440], [468, 469], [485, 486], [577, 597], [581, 597]], "functions": {"ResolveService.__init__": {"executed_lines": [32], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "ResolveService.resolve_thread": {"executed_lines": [50, 52, 53, 54, 55, 63, 64, 65, 68, 70, 71, 77, 78, 81, 82, 88, 89, 95, 97, 104, 120, 128], "summary": {"covered_lines": 22, "num_statements": 26, "percent_covered": 86.66666666666667, "percent_covered_display": "86.67", "missing_lines": 4, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [69, 105, 129, 131], "excluded_lines": [], "executed_branches": [[77, 78], [77, 81], [88, 89], [88, 95]], "missing_branches": []}, "ResolveService.unresolve_thread": {"executed_lines": [152, 154, 155, 165, 166, 167, 170, 172, 173, 179, 180, 183, 184, 190, 191, 197, 199, 206, 223, 231], "summary": {"covered_lines": 20, "num_statements": 26, "percent_covered": 80.0, "percent_covered_display": "80.00", "missing_lines": 6, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [156, 157, 171, 207, 232, 234], "excluded_lines": [], "executed_branches": [[179, 180], [179, 183], [190, 191], [190, 197]], "missing_branches": []}, "ResolveService._handle_graphql_errors": {"executed_lines": [254, 255, 264, 265, 266, 267, 274, 277, 279, 283, 287, 292, 302, 303, 305, 313, 316, 325, 327], "summary": {"covered_lines": 19, "num_statements": 26, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 7, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 2, "covered_branches": 8, "missing_branches": 2}, "missing_lines": [256, 269, 272, 306, 308, 328, 330], "excluded_lines": [], "executed_branches": [[255, 264], [265, 266], [265, 313], [267, 274], [279, 283], [279, 287], [287, 292], [287, 302]], "missing_branches": [[255, 256], [267, 269]]}, "ResolveService.validate_thread_exists": {"executed_lines": [356, 358, 360, 367, 374, 381, 391, 415, 422, 423, 424, 425, 433, 434, 435, 436, 437, 438, 442, 450, 459, 460, 462, 464, 467, 468, 475, 476, 482, 484, 485, 492, 493, 495, 496, 504, 506, 508, 509, 517, 530, 532], "summary": {"covered_lines": 42, "num_statements": 59, "percent_covered": 71.08433734939759, "percent_covered_display": "71.08", "missing_lines": 17, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 7, "covered_branches": 17, "missing_branches": 7}, "missing_lines": [361, 368, 375, 382, 426, 427, 440, 451, 452, 456, 469, 473, 486, 490, 533, 534, 543], "excluded_lines": [], "executed_branches": [[360, 367], [367, 374], [374, 381], [381, 391], [433, 434], [433, 459], [436, 437], [436, 442], [437, 438], [462, 464], [462, 467], [468, 475], [475, 476], [475, 484], [485, 492], [495, 496], [495, 506]], "missing_branches": [[360, 361], [367, 368], [374, 375], [381, 382], [437, 440], [468, 469], [485, 486]]}, "ResolveService._get_thread_url": {"executed_lines": [566, 571, 572, 573, 576, 577, 578, 579, 581, 582, 584, 590, 591, 597], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 2, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 2, "covered_branches": 6, "missing_branches": 2}, "missing_lines": [599, 601], "excluded_lines": [], "executed_branches": [[572, 573], [572, 576], [577, 578], [581, 582], [584, 590], [584, 597]], "missing_branches": [[577, 597], [581, 597]]}, "ResolveService._extract_thread_url_fragment": {"executed_lines": [612, 614, 615, 619], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [620, 622], "excluded_lines": [], "executed_branches": [[614, 615], [614, 619]], "missing_branches": []}, "ResolveService._build_fallback_url": {"executed_lines": [633, 635, 636, 637, 639, 643, 644, 647, 648], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[636, 637], [636, 647]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 15, 16, 23, 24, 26, 34, 136, 239, 335, 556, 603, 624], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ResolveService": {"executed_lines": [32, 50, 52, 53, 54, 55, 63, 64, 65, 68, 70, 71, 77, 78, 81, 82, 88, 89, 95, 97, 104, 120, 128, 152, 154, 155, 165, 166, 167, 170, 172, 173, 179, 180, 183, 184, 190, 191, 197, 199, 206, 223, 231, 254, 255, 264, 265, 266, 267, 274, 277, 279, 283, 287, 292, 302, 303, 305, 313, 316, 325, 327, 356, 358, 360, 367, 374, 381, 391, 415, 422, 423, 424, 425, 433, 434, 435, 436, 437, 438, 442, 450, 459, 460, 462, 464, 467, 468, 475, 476, 482, 484, 485, 492, 493, 495, 496, 504, 506, 508, 509, 517, 530, 532, 566, 571, 572, 573, 576, 577, 578, 579, 581, 582, 584, 590, 591, 597, 612, 614, 615, 619, 633, 635, 636, 637, 639, 643, 644, 647, 648], "summary": {"covered_lines": 131, "num_statements": 169, "percent_covered": 78.02690582959642, "percent_covered_display": "78.03", "missing_lines": 38, "excluded_lines": 0, "num_branches": 54, "num_partial_branches": 11, "covered_branches": 43, "missing_branches": 11}, "missing_lines": [69, 105, 129, 131, 156, 157, 171, 207, 232, 234, 256, 269, 272, 306, 308, 328, 330, 361, 368, 375, 382, 426, 427, 440, 451, 452, 456, 469, 473, 486, 490, 533, 534, 543, 599, 601, 620, 622], "excluded_lines": [], "executed_branches": [[77, 78], [77, 81], [88, 89], [88, 95], [179, 180], [179, 183], [190, 191], [190, 197], [255, 264], [265, 266], [265, 313], [267, 274], [279, 283], [279, 287], [287, 292], [287, 302], [360, 367], [367, 374], [374, 381], [381, 391], [433, 434], [433, 459], [436, 437], [436, 442], [437, 438], [462, 464], [462, 467], [468, 475], [475, 476], [475, 484], [485, 492], [495, 496], [495, 506], [572, 573], [572, 576], [577, 578], [581, 582], [584, 590], [584, 597], [614, 615], [614, 619], [636, 637], [636, 647]], "missing_branches": [[255, 256], [267, 269], [360, 361], [367, 368], [374, 375], [381, 382], [437, 440], [468, 469], [485, 486], [577, 597], [581, 597]]}, "": {"executed_lines": [1, 3, 4, 6, 15, 16, 23, 24, 26, 34, 136, 239, 335, 556, 603, 624], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/utils.py": {"executed_lines": [1, 3, 4, 6, 9, 12, 24, 26, 27, 28, 35, 36, 43, 46, 47, 48, 49, 51, 52, 54, 55, 56, 66, 71, 72, 73, 74, 75, 76, 77, 80, 90, 92, 94, 95, 97, 105, 118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 137, 138, 140, 141, 143], "summary": {"covered_lines": 51, "num_statements": 53, "percent_covered": 96.1038961038961, "percent_covered_display": "96.10", "missing_lines": 2, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [57, 58], "excluded_lines": [], "executed_branches": [[27, 28], [27, 35], [35, 36], [35, 43], [47, 48], [47, 49], [49, 51], [49, 52], [52, 54], [52, 66], [55, 56], [72, 73], [72, 80], [94, 95], [94, 97], [118, 119], [118, 121], [121, 122], [121, 124], [124, 125], [124, 127], [127, 128], [127, 130]], "missing_branches": [[55, 66]], "functions": {"parse_datetime": {"executed_lines": [24, 26, 27, 28, 35, 36, 43, 46, 47, 48, 49, 51, 52, 54, 55, 56, 66, 71, 72, 73, 74, 75, 76, 77, 80, 90, 92, 94, 95, 97], "summary": {"covered_lines": 30, "num_statements": 32, "percent_covered": 93.75, "percent_covered_display": "93.75", "missing_lines": 2, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 1, "covered_branches": 15, "missing_branches": 1}, "missing_lines": [57, 58], "excluded_lines": [], "executed_branches": [[27, 28], [27, 35], [35, 36], [35, 43], [47, 48], [47, 49], [49, 51], [49, 52], [52, 54], [52, 66], [55, 56], [72, 73], [72, 80], [94, 95], [94, 97]], "missing_branches": [[55, 66]]}, "emit_error": {"executed_lines": [118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 137, 138, 140, 141, 143], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[118, 119], [118, 121], [121, 122], [121, 124], [124, 125], [124, 127], [127, 128], [127, 130]], "missing_branches": []}, "": {"executed_lines": [1, 3, 4, 6, 9, 12, 105], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 9, 12, 24, 26, 27, 28, 35, 36, 43, 46, 47, 48, 49, 51, 52, 54, 55, 56, 66, 71, 72, 73, 74, 75, 76, 77, 80, 90, 92, 94, 95, 97, 105, 118, 119, 121, 122, 124, 125, 127, 128, 130, 131, 137, 138, 140, 141, 143], "summary": {"covered_lines": 51, "num_statements": 53, "percent_covered": 96.1038961038961, "percent_covered_display": "96.10", "missing_lines": 2, "excluded_lines": 0, "num_branches": 24, "num_partial_branches": 1, "covered_branches": 23, "missing_branches": 1}, "missing_lines": [57, 58], "excluded_lines": [], "executed_branches": [[27, 28], [27, 35], [35, 36], [35, 43], [47, 48], [47, 49], [49, 51], [49, 52], [52, 54], [52, 66], [55, 56], [72, 73], [72, 80], [94, 95], [94, 97], [118, 119], [118, 121], [121, 122], [121, 124], [124, 125], [124, 127], [127, 128], [127, 130]], "missing_branches": [[55, 66]]}}}, "src/toady/validators/__init__.py": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": [], "functions": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"": {"executed_lines": [1], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/validators/node_id_validation.py": {"executed_lines": [1, 8, 9, 10, 13, 14, 17, 18, 21, 22, 23, 26, 29, 30, 33, 34, 35, 38, 41, 44, 45, 48, 51, 52, 55, 61, 67, 69, 75, 78, 80, 86, 88, 97, 98, 99, 100, 102, 112, 113, 115, 116, 120, 121, 122, 124, 139, 140, 143, 144, 145, 146, 149, 150, 151, 157, 158, 160, 161, 166, 167, 173, 174, 179, 181, 196, 197, 199, 200, 201, 204, 205, 206, 209, 211, 220, 222, 224, 225, 226, 227, 229, 234, 238, 240, 243, 245, 248, 250, 253, 255, 259, 271, 272, 275, 287, 288, 291, 293, 294, 297, 299, 300], "summary": {"covered_lines": 100, "num_statements": 100, "percent_covered": 99.24242424242425, "percent_covered_display": "99.24", "missing_lines": 0, "excluded_lines": 0, "num_branches": 32, "num_partial_branches": 1, "covered_branches": 31, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[97, 98], [97, 100], [98, 97], [98, 99], [112, 113], [112, 115], [115, 116], [115, 120], [121, -102], [121, 122], [139, 140], [139, 143], [144, 145], [144, 149], [149, 150], [149, 157], [160, 161], [160, 166], [166, 167], [166, 173], [173, 174], [173, 179], [196, 197], [196, 199], [200, 201], [200, 204], [204, 205], [204, 209], [224, 225], [226, 227], [226, 229]], "missing_branches": [[224, 234]], "functions": {"NodeIDValidator.__init__": {"executed_lines": [75, 78], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "NodeIDValidator.get_allowed_prefixes": {"executed_lines": [86], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "NodeIDValidator.identify_entity_type": {"executed_lines": [97, 98, 99, 100], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[97, 98], [97, 100], [98, 97], [98, 99]], "missing_branches": []}, "NodeIDValidator.validate_numeric_id": {"executed_lines": [112, 113, 115, 116, 120, 121, 122], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[112, 113], [112, 115], [115, 116], [115, 120], [121, -102], [121, 122]], "missing_branches": []}, "NodeIDValidator.validate_node_id_format": {"executed_lines": [139, 140, 143, 144, 145, 146, 149, 150, 151, 157, 158, 160, 161, 166, 167, 173, 174, 179], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[139, 140], [139, 143], [144, 145], [144, 149], [149, 150], [149, 157], [160, 161], [160, 166], [166, 167], [166, 173], [173, 174], [173, 179]], "missing_branches": []}, "NodeIDValidator.validate_id": {"executed_lines": [196, 197, 199, 200, 201, 204, 205, 206, 209], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[196, 197], [196, 199], [200, 201], [200, 204], [204, 205], [204, 209]], "missing_branches": []}, "NodeIDValidator.format_allowed_types_message": {"executed_lines": [220, 222, 224, 225, 226, 227, 229, 234], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 91.66666666666667, "percent_covered_display": "91.67", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[224, 225], [226, 227], [226, 229]], "missing_branches": [[224, 234]]}, "create_comment_validator": {"executed_lines": [240], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_thread_validator": {"executed_lines": [245], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_review_validator": {"executed_lines": [250], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "create_universal_validator": {"executed_lines": [255], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_comment_id": {"executed_lines": [271, 272], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_thread_id": {"executed_lines": [287, 288], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "get_comment_id_format_message": {"executed_lines": [293, 294], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "get_thread_id_format_message": {"executed_lines": [299, 300], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 8, 9, 10, 13, 14, 17, 18, 21, 22, 23, 26, 29, 30, 33, 34, 35, 38, 41, 44, 45, 48, 51, 52, 55, 61, 67, 69, 80, 88, 102, 124, 181, 211, 238, 243, 248, 253, 259, 275, 291, 297], "summary": {"covered_lines": 39, "num_statements": 39, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"GitHubEntityType": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "NodeIDValidator": {"executed_lines": [75, 78, 86, 97, 98, 99, 100, 112, 113, 115, 116, 120, 121, 122, 139, 140, 143, 144, 145, 146, 149, 150, 151, 157, 158, 160, 161, 166, 167, 173, 174, 179, 196, 197, 199, 200, 201, 204, 205, 206, 209, 220, 222, 224, 225, 226, 227, 229, 234], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 98.76543209876543, "percent_covered_display": "98.77", "missing_lines": 0, "excluded_lines": 0, "num_branches": 32, "num_partial_branches": 1, "covered_branches": 31, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[97, 98], [97, 100], [98, 97], [98, 99], [112, 113], [112, 115], [115, 116], [115, 120], [121, -102], [121, 122], [139, 140], [139, 143], [144, 145], [144, 149], [149, 150], [149, 157], [160, 161], [160, 166], [166, 167], [166, 173], [173, 174], [173, 179], [196, 197], [196, 199], [200, 201], [200, 204], [204, 205], [204, 209], [224, 225], [226, 227], [226, 229]], "missing_branches": [[224, 234]]}, "": {"executed_lines": [1, 8, 9, 10, 13, 14, 17, 18, 21, 22, 23, 26, 29, 30, 33, 34, 35, 38, 41, 44, 45, 48, 51, 52, 55, 61, 67, 69, 80, 88, 102, 124, 181, 211, 238, 240, 243, 245, 248, 250, 253, 255, 259, 271, 272, 275, 287, 288, 291, 293, 294, 297, 299, 300], "summary": {"covered_lines": 51, "num_statements": 51, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/validators/schema_validator.py": {"executed_lines": [1, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 22, 23, 25, 38, 39, 40, 43, 44, 48, 65, 76, 77, 78, 79, 80, 82, 84, 85, 87, 89, 91, 93, 94, 95, 97, 98, 99, 101, 102, 106, 108, 109, 111, 112, 115, 116, 117, 118, 119, 120, 121, 123, 125, 126, 129, 130, 133, 139, 140, 142, 155, 156, 157, 158, 159, 160, 161, 163, 165, 167, 172, 174, 175, 177, 178, 179, 180, 181, 183, 184, 188, 190, 192, 193, 195, 197, 200, 201, 202, 203, 205, 214, 217, 219, 228, 229, 231, 234, 235, 241, 244, 245, 246, 247, 248, 254, 257, 258, 259, 269, 273, 275, 290, 294, 295, 298, 300, 302, 316, 318, 320, 321, 332, 335, 336, 337, 338, 348, 353, 355, 356, 357, 358, 359, 366, 381, 384, 385, 386, 387, 397, 398, 402, 403, 412, 421, 422, 425, 426, 428, 430, 439, 441, 463, 469, 473, 474, 476, 486, 487, 488, 490, 493, 499, 501, 507, 514, 517, 518, 522, 523, 527, 528, 529, 531, 532, 533, 535, 537, 543, 545, 548, 551, 552, 553, 554, 556, 558, 564, 574, 575, 576, 577, 578, 579, 580, 581, 583, 584, 588], "summary": {"covered_lines": 198, "num_statements": 224, "percent_covered": 83.98692810457516, "percent_covered_display": "83.99", "missing_lines": 26, "excluded_lines": 0, "num_branches": 82, "num_partial_branches": 19, "covered_branches": 59, "missing_branches": 23}, "missing_lines": [103, 104, 113, 186, 189, 191, 198, 215, 260, 266, 291, 296, 305, 306, 307, 308, 314, 450, 451, 453, 458, 459, 461, 470, 519, 524], "excluded_lines": [], "executed_branches": [[94, 95], [94, 97], [108, 109], [108, 111], [112, 115], [155, 156], [155, 163], [157, 158], [157, 163], [174, 175], [174, 177], [178, 179], [197, 200], [201, -195], [201, 202], [202, 203], [214, 217], [228, 229], [228, 231], [234, 235], [234, 244], [259, 269], [290, 294], [295, 298], [300, -275], [300, 302], [302, 316], [318, 320], [318, 335], [335, 336], [335, 348], [353, 300], [353, 355], [356, 357], [358, 300], [358, 359], [384, 385], [384, 397], [385, 384], [385, 386], [397, -366], [397, 398], [398, 397], [398, 402], [421, 422], [421, 425], [425, 426], [425, 428], [469, 473], [487, 488], [487, 490], [518, 522], [523, 527], [528, 529], [532, 533], [553, 554], [577, 578], [579, 580], [583, 584]], "missing_branches": [[112, 113], [178, 186], [197, 198], [202, 201], [214, 215], [259, 260], [290, 291], [295, 296], [302, 305], [307, 308], [307, 314], [356, 300], [450, 451], [450, 453], [469, 470], [518, 519], [523, 524], [528, 531], [532, 535], [553, 556], [577, 579], [579, 583], [583, 588]], "functions": {"SchemaValidationError.__init__": {"executed_lines": [38, 39, 40], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator.__init__": {"executed_lines": [76, 77, 78, 79, 80], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator._get_cache_path": {"executed_lines": [84, 85], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator._get_cache_metadata_path": {"executed_lines": [89], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator._is_cache_valid": {"executed_lines": [93, 94, 95, 97, 98, 99, 101, 102], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 2, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [103, 104], "excluded_lines": [], "executed_branches": [[94, 95], [94, 97]], "missing_branches": []}, "GitHubSchemaValidator._load_cached_schema": {"executed_lines": [108, 109, 111, 112, 115, 116, 117, 118, 119, 120, 121], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 87.5, "percent_covered_display": "87.50", "missing_lines": 1, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 1, "covered_branches": 3, "missing_branches": 1}, "missing_lines": [113], "excluded_lines": [], "executed_branches": [[108, 109], [108, 111], [112, 115]], "missing_branches": [[112, 113]]}, "GitHubSchemaValidator._save_schema_to_cache": {"executed_lines": [125, 126, 129, 130, 133, 139, 140], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator.fetch_schema": {"executed_lines": [155, 156, 157, 158, 159, 160, 161, 163, 165, 167, 172, 174, 175, 177, 178, 179, 180, 181, 183, 184, 188, 190, 192, 193], "summary": {"covered_lines": 24, "num_statements": 27, "percent_covered": 88.57142857142857, "percent_covered_display": "88.57", "missing_lines": 3, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 1, "covered_branches": 7, "missing_branches": 1}, "missing_lines": [186, 189, 191], "excluded_lines": [], "executed_branches": [[155, 156], [155, 163], [157, 158], [157, 163], [174, 175], [174, 177], [178, 179]], "missing_branches": [[178, 186]]}, "GitHubSchemaValidator._build_type_map": {"executed_lines": [197, 200, 201, 202, 203], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 75.0, "percent_covered_display": "75.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 2, "covered_branches": 4, "missing_branches": 2}, "missing_lines": [198], "excluded_lines": [], "executed_branches": [[197, 200], [201, -195], [201, 202], [202, 203]], "missing_branches": [[197, 198], [202, 201]]}, "GitHubSchemaValidator.get_type": {"executed_lines": [214, 217], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 60.0, "percent_covered_display": "60.00", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [215], "excluded_lines": [], "executed_branches": [[214, 217]], "missing_branches": [[214, 215]]}, "GitHubSchemaValidator.validate_query": {"executed_lines": [228, 229, 231, 234, 235, 241, 244, 245, 246, 247, 248, 254, 257, 258, 259, 269, 273], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 88.0, "percent_covered_display": "88.00", "missing_lines": 2, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 1, "covered_branches": 5, "missing_branches": 1}, "missing_lines": [260, 266], "excluded_lines": [], "executed_branches": [[228, 229], [228, 231], [234, 235], [234, 244], [259, 269]], "missing_branches": [[259, 260]]}, "GitHubSchemaValidator._validate_selections": {"executed_lines": [290, 294, 295, 298, 300, 302, 316, 318, 320, 321, 332, 335, 336, 337, 338, 348, 353, 355, 356, 357, 358, 359], "summary": {"covered_lines": 22, "num_statements": 29, "percent_covered": 73.46938775510205, "percent_covered_display": "73.47", "missing_lines": 7, "excluded_lines": 0, "num_branches": 20, "num_partial_branches": 4, "covered_branches": 14, "missing_branches": 6}, "missing_lines": [291, 296, 305, 306, 307, 308, 314], "excluded_lines": [], "executed_branches": [[290, 294], [295, 298], [300, -275], [300, 302], [302, 316], [318, 320], [318, 335], [335, 336], [335, 348], [353, 300], [353, 355], [356, 357], [358, 300], [358, 359]], "missing_branches": [[290, 291], [295, 296], [302, 305], [307, 308], [307, 314], [356, 300]]}, "GitHubSchemaValidator._validate_arguments": {"executed_lines": [381, 384, 385, 386, 387, 397, 398, 402, 403], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[384, 385], [384, 397], [385, 384], [385, 386], [397, -366], [397, 398], [398, 397], [398, 402]], "missing_branches": []}, "GitHubSchemaValidator._resolve_field_type": {"executed_lines": [421, 422, 425, 426, 428], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[421, 422], [421, 425], [425, 426], [425, 428]], "missing_branches": []}, "GitHubSchemaValidator._is_required_type": {"executed_lines": [439], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator.check_deprecations": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0.00", "missing_lines": 6, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 2}, "missing_lines": [450, 451, 453, 458, 459, 461], "excluded_lines": [], "executed_branches": [], "missing_branches": [[450, 451], [450, 453]]}, "GitHubSchemaValidator.get_schema_version": {"executed_lines": [469, 473, 474], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 66.66666666666667, "percent_covered_display": "66.67", "missing_lines": 1, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [470], "excluded_lines": [], "executed_branches": [[469, 473]], "missing_branches": [[469, 470]]}, "GitHubSchemaValidator.get_field_suggestions": {"executed_lines": [486, 487, 488, 490, 493, 499], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 0, "covered_branches": 2, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[487, 488], [487, 490]], "missing_branches": []}, "GitHubSchemaValidator.validate_mutations": {"executed_lines": [507, 514, 517, 518, 522, 523, 527, 528, 529, 531, 532, 533, 535], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 73.91304347826087, "percent_covered_display": "73.91", "missing_lines": 2, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 4, "covered_branches": 4, "missing_branches": 4}, "missing_lines": [519, 524], "excluded_lines": [], "executed_branches": [[518, 522], [523, 527], [528, 529], [532, 533]], "missing_branches": [[518, 519], [523, 524], [528, 531], [532, 535]]}, "GitHubSchemaValidator.validate_queries": {"executed_lines": [543, 545, 548, 551, 552, 553, 554, 556], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 90.0, "percent_covered_display": "90.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 2, "num_partial_branches": 1, "covered_branches": 1, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[553, 554]], "missing_branches": [[553, 556]]}, "GitHubSchemaValidator.generate_compatibility_report": {"executed_lines": [564, 574, 575, 576, 577, 578, 579, 580, 581, 583, 584, 588], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 83.33333333333333, "percent_covered_display": "83.33", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 3, "covered_branches": 3, "missing_branches": 3}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[577, 578], [579, 580], [583, 584]], "missing_branches": [[577, 579], [579, 583], [583, 588]]}, "": {"executed_lines": [1, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 22, 23, 25, 43, 44, 48, 65, 82, 87, 91, 106, 123, 142, 195, 205, 219, 275, 366, 412, 430, 441, 463, 476, 501, 537, 558], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"SchemaValidationError": {"executed_lines": [38, 39, 40], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "GitHubSchemaValidator": {"executed_lines": [76, 77, 78, 79, 80, 84, 85, 89, 93, 94, 95, 97, 98, 99, 101, 102, 108, 109, 111, 112, 115, 116, 117, 118, 119, 120, 121, 125, 126, 129, 130, 133, 139, 140, 155, 156, 157, 158, 159, 160, 161, 163, 165, 167, 172, 174, 175, 177, 178, 179, 180, 181, 183, 184, 188, 190, 192, 193, 197, 200, 201, 202, 203, 214, 217, 228, 229, 231, 234, 235, 241, 244, 245, 246, 247, 248, 254, 257, 258, 259, 269, 273, 290, 294, 295, 298, 300, 302, 316, 318, 320, 321, 332, 335, 336, 337, 338, 348, 353, 355, 356, 357, 358, 359, 381, 384, 385, 386, 387, 397, 398, 402, 403, 421, 422, 425, 426, 428, 439, 469, 473, 474, 486, 487, 488, 490, 493, 499, 507, 514, 517, 518, 522, 523, 527, 528, 529, 531, 532, 533, 535, 543, 545, 548, 551, 552, 553, 554, 556, 564, 574, 575, 576, 577, 578, 579, 580, 581, 583, 584, 588], "summary": {"covered_lines": 161, "num_statements": 187, "percent_covered": 81.78438661710037, "percent_covered_display": "81.78", "missing_lines": 26, "excluded_lines": 0, "num_branches": 82, "num_partial_branches": 19, "covered_branches": 59, "missing_branches": 23}, "missing_lines": [103, 104, 113, 186, 189, 191, 198, 215, 260, 266, 291, 296, 305, 306, 307, 308, 314, 450, 451, 453, 458, 459, 461, 470, 519, 524], "excluded_lines": [], "executed_branches": [[94, 95], [94, 97], [108, 109], [108, 111], [112, 115], [155, 156], [155, 163], [157, 158], [157, 163], [174, 175], [174, 177], [178, 179], [197, 200], [201, -195], [201, 202], [202, 203], [214, 217], [228, 229], [228, 231], [234, 235], [234, 244], [259, 269], [290, 294], [295, 298], [300, -275], [300, 302], [302, 316], [318, 320], [318, 335], [335, 336], [335, 348], [353, 300], [353, 355], [356, 357], [358, 300], [358, 359], [384, 385], [384, 397], [385, 384], [385, 386], [397, -366], [397, 398], [398, 397], [398, 402], [421, 422], [421, 425], [425, 426], [425, 428], [469, 473], [487, 488], [487, 490], [518, 522], [523, 527], [528, 529], [532, 533], [553, 554], [577, 578], [579, 580], [583, 584]], "missing_branches": [[112, 113], [178, 186], [197, 198], [202, 201], [214, 215], [259, 260], [290, 291], [295, 296], [302, 305], [307, 308], [307, 314], [356, 300], [450, 451], [450, 453], [469, 470], [518, 519], [523, 524], [528, 531], [532, 535], [553, 556], [577, 579], [579, 583], [583, 588]]}, "": {"executed_lines": [1, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 22, 23, 25, 43, 44, 48, 65, 82, 87, 91, 106, 123, 142, 195, 205, 219, 275, 366, 412, 430, 441, 463, 476, 501, 537, 558], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}}, "src/toady/validators/validation.py": {"executed_lines": [1, 7, 8, 9, 10, 12, 13, 14, 21, 22, 23, 24, 25, 28, 29, 32, 35, 52, 70, 71, 72, 73, 81, 82, 83, 84, 91, 92, 99, 100, 110, 111, 118, 119, 126, 127, 137, 140, 158, 159, 167, 168, 169, 176, 177, 179, 180, 183, 184, 185, 186, 193, 196, 211, 212, 220, 221, 222, 229, 230, 231, 232, 233, 240, 243, 263, 264, 271, 272, 280, 283, 284, 292, 293, 300, 301, 312, 313, 314, 328, 329, 330, 340, 343, 363, 364, 372, 373, 374, 375, 382, 383, 390, 391, 401, 402, 409, 410, 417, 418, 425, 428, 441, 442, 449, 450, 457, 458, 459, 466, 467, 469, 470, 472, 474, 481, 489, 502, 503, 510, 511, 518, 519, 520, 527, 528, 535, 538, 551, 552, 559, 560, 567, 568, 569, 576, 577, 584, 587, 600, 601, 608, 609, 616, 617, 618, 625, 626, 635, 638, 658, 659, 666, 667, 674, 675, 676, 683, 684, 691, 692, 699, 702, 718, 719, 720, 721, 728, 729, 731, 732, 733, 734, 735, 736, 738, 746, 766, 767, 775, 776, 779, 780, 781, 782, 783, 785, 793, 813, 814, 821, 822, 830, 831, 832, 845, 846, 847, 848, 849, 862, 866, 875, 878, 879, 882, 883, 886, 887, 890, 891, 893, 897, 917, 925, 945, 953, 954, 955, 961, 962, 963, 964, 965, 968, 987, 988, 990, 991, 996, 997, 1003, 1004, 1009], "summary": {"covered_lines": 232, "num_statements": 236, "percent_covered": 98.6842105263158, "percent_covered_display": "98.68", "missing_lines": 4, "excluded_lines": 0, "num_branches": 144, "num_partial_branches": 1, "covered_branches": 143, "missing_branches": 1}, "missing_lines": [101, 102, 392, 393], "excluded_lines": [], "executed_branches": [[70, 71], [70, 81], [71, 72], [71, 73], [81, 82], [81, 110], [83, 84], [83, 91], [91, 92], [91, 99], [110, 111], [110, 118], [118, 119], [118, 126], [126, 127], [126, 137], [158, 159], [158, 167], [168, 169], [168, 176], [177, 179], [177, 183], [211, 212], [211, 220], [221, 222], [221, 229], [263, 264], [263, 271], [271, 272], [271, 280], [283, 284], [283, 292], [292, 293], [292, 300], [300, 301], [300, 312], [313, 314], [313, 328], [329, 330], [329, 340], [363, 364], [363, 372], [372, 373], [372, 401], [374, 375], [374, 382], [382, 383], [382, 390], [401, 402], [401, 409], [409, 410], [409, 417], [417, 418], [417, 425], [441, 442], [441, 449], [449, 450], [449, 457], [458, 459], [458, 466], [472, 474], [472, 481], [502, 503], [502, 510], [510, 511], [510, 518], [519, 520], [519, 527], [527, 528], [527, 535], [551, 552], [551, 559], [559, 560], [559, 567], [568, 569], [568, 576], [576, 577], [576, 584], [600, 601], [600, 608], [608, 609], [608, 616], [617, 618], [617, 625], [625, 626], [625, 635], [658, 659], [658, 666], [666, 667], [666, 674], [675, 676], [675, 683], [683, 684], [683, 691], [691, 692], [691, 699], [718, 719], [718, 728], [719, 720], [719, 721], [728, 729], [728, 731], [731, 732], [731, 738], [733, 734], [733, 735], [735, 736], [735, 738], [766, 767], [766, 775], [775, 776], [775, 779], [779, 780], [779, 785], [781, 782], [782, 781], [782, 783], [813, 814], [813, 821], [821, 822], [821, 830], [831, 832], [831, 845], [845, 846], [845, 862], [848, 849], [848, 862], [878, 879], [878, 882], [882, 883], [882, 886], [886, 887], [886, 890], [890, 891], [890, 893], [987, 988], [987, 990], [990, 991], [990, 996], [996, 997], [996, 1003], [1003, 1004], [1003, 1009]], "missing_branches": [[781, 785]], "functions": {"validate_pr_number": {"executed_lines": [70, 71, 72, 73, 81, 82, 83, 84, 91, 92, 99, 100, 110, 111, 118, 119, 126, 127, 137], "summary": {"covered_lines": 19, "num_statements": 21, "percent_covered": 94.5945945945946, "percent_covered_display": "94.59", "missing_lines": 2, "excluded_lines": 0, "num_branches": 16, "num_partial_branches": 0, "covered_branches": 16, "missing_branches": 0}, "missing_lines": [101, 102], "excluded_lines": [], "executed_branches": [[70, 71], [70, 81], [71, 72], [71, 73], [81, 82], [81, 110], [83, 84], [83, 91], [91, 92], [91, 99], [110, 111], [110, 118], [118, 119], [118, 126], [126, 127], [126, 137]], "missing_branches": []}, "validate_comment_id": {"executed_lines": [158, 159, 167, 168, 169, 176, 177, 179, 180, 183, 184, 185, 186, 193], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 6, "num_partial_branches": 0, "covered_branches": 6, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[158, 159], [158, 167], [168, 169], [168, 176], [177, 179], [177, 183]], "missing_branches": []}, "validate_thread_id": {"executed_lines": [211, 212, 220, 221, 222, 229, 230, 231, 232, 233, 240], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 4, "num_partial_branches": 0, "covered_branches": 4, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[211, 212], [211, 220], [221, 222], [221, 229]], "missing_branches": []}, "validate_reply_body": {"executed_lines": [263, 264, 271, 272, 280, 283, 284, 292, 293, 300, 301, 312, 313, 314, 328, 329, 330, 340], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 0, "covered_branches": 14, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[263, 264], [263, 271], [271, 272], [271, 280], [283, 284], [283, 292], [292, 293], [292, 300], [300, 301], [300, 312], [313, 314], [313, 328], [329, 330], [329, 340]], "missing_branches": []}, "validate_limit": {"executed_lines": [363, 364, 372, 373, 374, 375, 382, 383, 390, 391, 401, 402, 409, 410, 417, 418, 425], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 93.93939393939394, "percent_covered_display": "93.94", "missing_lines": 2, "excluded_lines": 0, "num_branches": 14, "num_partial_branches": 0, "covered_branches": 14, "missing_branches": 0}, "missing_lines": [392, 393], "excluded_lines": [], "executed_branches": [[363, 364], [363, 372], [372, 373], [372, 401], [374, 375], [374, 382], [382, 383], [382, 390], [401, 402], [401, 409], [409, 410], [409, 417], [417, 418], [417, 425]], "missing_branches": []}, "validate_datetime_string": {"executed_lines": [441, 442, 449, 450, 457, 458, 459, 466, 467, 469, 470, 472, 474, 481], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[441, 442], [441, 449], [449, 450], [449, 457], [458, 459], [458, 466], [472, 474], [472, 481]], "missing_branches": []}, "validate_email": {"executed_lines": [502, 503, 510, 511, 518, 519, 520, 527, 528, 535], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[502, 503], [502, 510], [510, 511], [510, 518], [519, 520], [519, 527], [527, 528], [527, 535]], "missing_branches": []}, "validate_url": {"executed_lines": [551, 552, 559, 560, 567, 568, 569, 576, 577, 584], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[551, 552], [551, 559], [559, 560], [559, 567], [568, 569], [568, 576], [576, 577], [576, 584]], "missing_branches": []}, "validate_username": {"executed_lines": [600, 601, 608, 609, 616, 617, 618, 625, 626, 635], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[600, 601], [600, 608], [608, 609], [608, 616], [617, 618], [617, 625], [625, 626], [625, 635]], "missing_branches": []}, "validate_non_empty_string": {"executed_lines": [658, 659, 666, 667, 674, 675, 676, 683, 684, 691, 692, 699], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[658, 659], [658, 666], [666, 667], [666, 674], [675, 676], [675, 683], [683, 684], [683, 691], [691, 692], [691, 699]], "missing_branches": []}, "validate_boolean_flag": {"executed_lines": [718, 719, 720, 721, 728, 729, 731, 732, 733, 734, 735, 736, 738], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 12, "num_partial_branches": 0, "covered_branches": 12, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[718, 719], [718, 728], [719, 720], [719, 721], [728, 729], [728, 731], [731, 732], [731, 738], [733, 734], [733, 735], [735, 736], [735, 738]], "missing_branches": []}, "validate_choice": {"executed_lines": [766, 767, 775, 776, 779, 780, 781, 782, 783, 785], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 95.0, "percent_covered_display": "95.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 1, "covered_branches": 9, "missing_branches": 1}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[766, 767], [766, 775], [775, 776], [775, 779], [779, 780], [779, 785], [781, 782], [782, 781], [782, 783]], "missing_branches": [[781, 785]]}, "validate_dict_keys": {"executed_lines": [813, 814, 821, 822, 830, 831, 832, 845, 846, 847, 848, 849, 862], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 10, "num_partial_branches": 0, "covered_branches": 10, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[813, 814], [813, 821], [821, 822], [821, 830], [831, 832], [831, 845], [845, 846], [845, 862], [848, 849], [848, 862]], "missing_branches": []}, "validate_reply_content_warnings": {"executed_lines": [875, 878, 879, 882, 883, 886, 887, 890, 891, 893], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[878, 879], [878, 882], [882, 883], [882, 886], [886, 887], [886, 890], [890, 891], [890, 893]], "missing_branches": []}, "validate_fetch_command_args": {"executed_lines": [917], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_reply_command_args": {"executed_lines": [945], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "validate_resolve_command_args": {"executed_lines": [987, 988, 990, 991, 996, 997, 1003, 1004, 1009], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 8, "num_partial_branches": 0, "covered_branches": 8, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [[987, 988], [987, 990], [990, 991], [990, 996], [996, 997], [996, 1003], [1003, 1004], [1003, 1009]], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 10, 12, 13, 14, 21, 22, 23, 24, 25, 28, 29, 32, 35, 52, 140, 196, 243, 343, 428, 489, 538, 587, 638, 702, 746, 793, 866, 897, 925, 953, 954, 955, 961, 962, 963, 964, 965, 968], "summary": {"covered_lines": 40, "num_statements": 40, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}}, "classes": {"ResolveOptions": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100.00", "missing_lines": 0, "excluded_lines": 0, "num_branches": 0, "num_partial_branches": 0, "covered_branches": 0, "missing_branches": 0}, "missing_lines": [], "excluded_lines": [], "executed_branches": [], "missing_branches": []}, "": {"executed_lines": [1, 7, 8, 9, 10, 12, 13, 14, 21, 22, 23, 24, 25, 28, 29, 32, 35, 52, 70, 71, 72, 73, 81, 82, 83, 84, 91, 92, 99, 100, 110, 111, 118, 119, 126, 127, 137, 140, 158, 159, 167, 168, 169, 176, 177, 179, 180, 183, 184, 185, 186, 193, 196, 211, 212, 220, 221, 222, 229, 230, 231, 232, 233, 240, 243, 263, 264, 271, 272, 280, 283, 284, 292, 293, 300, 301, 312, 313, 314, 328, 329, 330, 340, 343, 363, 364, 372, 373, 374, 375, 382, 383, 390, 391, 401, 402, 409, 410, 417, 418, 425, 428, 441, 442, 449, 450, 457, 458, 459, 466, 467, 469, 470, 472, 474, 481, 489, 502, 503, 510, 511, 518, 519, 520, 527, 528, 535, 538, 551, 552, 559, 560, 567, 568, 569, 576, 577, 584, 587, 600, 601, 608, 609, 616, 617, 618, 625, 626, 635, 638, 658, 659, 666, 667, 674, 675, 676, 683, 684, 691, 692, 699, 702, 718, 719, 720, 721, 728, 729, 731, 732, 733, 734, 735, 736, 738, 746, 766, 767, 775, 776, 779, 780, 781, 782, 783, 785, 793, 813, 814, 821, 822, 830, 831, 832, 845, 846, 847, 848, 849, 862, 866, 875, 878, 879, 882, 883, 886, 887, 890, 891, 893, 897, 917, 925, 945, 953, 954, 955, 961, 962, 963, 964, 965, 968, 987, 988, 990, 991, 996, 997, 1003, 1004, 1009], "summary": {"covered_lines": 232, "num_statements": 236, "percent_covered": 98.6842105263158, "percent_covered_display": "98.68", "missing_lines": 4, "excluded_lines": 0, "num_branches": 144, "num_partial_branches": 1, "covered_branches": 143, "missing_branches": 1}, "missing_lines": [101, 102, 392, 393], "excluded_lines": [], "executed_branches": [[70, 71], [70, 81], [71, 72], [71, 73], [81, 82], [81, 110], [83, 84], [83, 91], [91, 92], [91, 99], [110, 111], [110, 118], [118, 119], [118, 126], [126, 127], [126, 137], [158, 159], [158, 167], [168, 169], [168, 176], [177, 179], [177, 183], [211, 212], [211, 220], [221, 222], [221, 229], [263, 264], [263, 271], [271, 272], [271, 280], [283, 284], [283, 292], [292, 293], [292, 300], [300, 301], [300, 312], [313, 314], [313, 328], [329, 330], [329, 340], [363, 364], [363, 372], [372, 373], [372, 401], [374, 375], [374, 382], [382, 383], [382, 390], [401, 402], [401, 409], [409, 410], [409, 417], [417, 418], [417, 425], [441, 442], [441, 449], [449, 450], [449, 457], [458, 459], [458, 466], [472, 474], [472, 481], [502, 503], [502, 510], [510, 511], [510, 518], [519, 520], [519, 527], [527, 528], [527, 535], [551, 552], [551, 559], [559, 560], [559, 567], [568, 569], [568, 576], [576, 577], [576, 584], [600, 601], [600, 608], [608, 609], [608, 616], [617, 618], [617, 625], [625, 626], [625, 635], [658, 659], [658, 666], [666, 667], [666, 674], [675, 676], [675, 683], [683, 684], [683, 691], [691, 692], [691, 699], [718, 719], [718, 728], [719, 720], [719, 721], [728, 729], [728, 731], [731, 732], [731, 738], [733, 734], [733, 735], [735, 736], [735, 738], [766, 767], [766, 775], [775, 776], [775, 779], [779, 780], [779, 785], [781, 782], [782, 781], [782, 783], [813, 814], [813, 821], [821, 822], [821, 830], [831, 832], [831, 845], [845, 846], [845, 862], [848, 849], [848, 862], [878, 879], [878, 882], [882, 883], [882, 886], [886, 887], [886, 890], [890, 891], [890, 893], [987, 988], [987, 990], [990, 991], [990, 996], [996, 997], [996, 1003], [1003, 1004], [1003, 1009]], "missing_branches": [[781, 785]]}}}}, "totals": {"covered_lines": 3698, "num_statements": 4083, "percent_covered": 89.11685994647637, "percent_covered_display": "89.12", "missing_lines": 385, "excluded_lines": 68, "num_branches": 1522, "num_partial_branches": 145, "covered_branches": 1297, "missing_branches": 225}} diff --git a/pyproject.toml b/pyproject.toml index 1c0de08..6af5f4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,6 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -30,46 +29,61 @@ classifiers = [ "Topic :: Software Development :: Quality Assurance", "Environment :: Console", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ - "click>=8.1.0", - "rich>=13.0.0", # For pretty output formatting - "typing-extensions>=4.0.0;python_version<'3.10'", + "click>=8.1.7", + "rich>=13.9.0", # For pretty output formatting + "rich-click>=1.8.0", # Enhanced click with rich formatting + "typing-extensions>=4.12.0;python_version<'3.11'", ] [project.optional-dependencies] +# Development dependencies - latest versions as of Dec 2024 dev = [ - "pytest>=7.4.0", - "pytest-cov>=4.1.0", - "pytest-mock>=3.11.0", - "pytest-xdist>=3.3.0", - "pytest-timeout>=2.1.0", - "pytest-benchmark>=4.0.0", - "pytest-html>=3.2.0", - "pytest-json-report>=1.5.0", - "pytest-clarity>=1.0.1", - "pytest-sugar>=0.9.7", - "pytest-emoji>=0.2.0", - "pytest-randomly>=3.12.0", - "pytest-memorywatch>=0.6.0", - "pytest-profiling>=1.7.0", - "pytest-deadfixtures>=2.2.1", - "black>=23.0.0", - "ruff>=0.1.0", - "mypy>=1.5.0", - "pre-commit>=3.4.0", - "tox>=4.0.0", - "build>=1.0.0", - "twine>=4.0.0", - "tomli>=2.0.0;python_version<'3.11'", - "coverage[toml]>=7.3.0", + # Testing framework + "pytest>=8.3.0", + "pytest-cov>=6.0.0", + "pytest-mock>=3.14.0", + "pytest-xdist>=3.6.0", # Parallel test execution + "pytest-timeout>=2.3.1", # Test timeouts + + # Code quality + "black>=24.8.0", + "ruff>=0.8.0", # Much faster than flake8/isort + "mypy>=1.13.0", + "pre-commit>=4.0.0", + + # Type stubs + "types-click>=7.1.8", + "types-psutil>=6.1.0", + + # Build and publish + "build>=1.2.0", + "twine>=6.0.0", + + # Development utilities + "python-dotenv>=1.0.1", + "psutil>=6.1.0", + + # Documentation (optional) + "mkdocs>=1.6.0", + "mkdocs-material>=9.5.0", ] + +# Separate test dependencies for CI efficiency test = [ - "pytest>=7.4.0", - "pytest-cov>=4.1.0", - "pytest-mock>=3.11.0", - "pytest-xdist>=3.3.0", - "pytest-timeout>=2.1.0", + "pytest>=8.3.0", + "pytest-cov>=6.0.0", + "pytest-mock>=3.14.0", + "pytest-xdist>=3.6.0", + "python-dotenv>=1.0.1", +] + +# Documentation dependencies +docs = [ + "mkdocs>=1.6.0", + "mkdocs-material>=9.5.0", + "mkdocs-click>=0.8.1", ] [project.urls] @@ -89,35 +103,69 @@ toady = ["py.typed"] [tool.black] line-length = 88 -target-version = ['py38', 'py39', 'py310', 'py311', 'py312'] +target-version = ['py39', 'py310', 'py311', 'py312'] include = '\.pyi?$' [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 88 +extend-exclude = [ + ".venv", + "venv", + "__pycache__", + ".pytest_cache", + "htmlcov", + "build", + "dist", +] [tool.ruff.lint] +# Enable core linting rules (focused on critical issues) select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "C4", # flake8-comprehensions - "UP", # pyupgrade - "ARG", # flake8-unused-arguments - "SIM", # flake8-simplify + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear (critical bugs) + "C4", # flake8-comprehensions + "UP", # pyupgrade (basic Python version compatibility) +] + +ignore = [ + "FBT001", # Boolean positional arg in function definition + "FBT002", # Boolean default positional argument in function definition + "TRY003", # Avoid specifying long messages outside the exception class + "EM101", # Exception must not use a string literal + "EM102", # Exception must not use an f-string literal + "G004", # Logging statement uses f-string + "COM812", # Missing trailing comma (conflicts with formatter) + "ISC001", # Implicitly concatenated string literals (conflicts with formatter) ] -ignore = [] + fixable = ["ALL"] unfixable = [] [tool.ruff.lint.per-file-ignores] -"tests/*" = ["ARG001", "ARG002"] +"tests/*" = [ + "ARG001", # Unused function argument + "ARG002", # Unused method argument + "S101", # Use of assert detected + "PLR2004", # Magic value used in comparison + "SLF001", # Private member accessed + "FBT", # Boolean trap +] "src/toady/cli.py" = ["ARG001"] # Ignore unused arguments in CLI stubs +"conftest.py" = ["ARG001"] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.isort] +known-first-party = ["toady"] +force-sort-within-sections = true [tool.mypy] -python_version = "3.8" +python_version = "3.9" warn_return_any = true warn_unused_configs = true disallow_untyped_defs = true @@ -132,24 +180,22 @@ warn_unreachable = true strict_equality = true [tool.pytest.ini_options] -minversion = "7.0" +minversion = "8.0" addopts = [ "-ra", "--strict-markers", "--strict-config", "--color=yes", "--tb=short", - "--maxfail=1", - "--durations=10", - "--durations-min=1.0", - "--cov=toady", + "--cov=src/toady", # Updated for uv/modern structure "--cov-branch", "--cov-report=term-missing:skip-covered", "--cov-report=html:htmlcov", "--cov-report=xml:coverage.xml", - "--cov-fail-under=80", + "--cov-fail-under=90", # Raised from 80% since you improved coverage "-p", "no:warnings", - "--disable-warnings", + "--maxfail=10", # Stop after 10 failures + "--timeout=300", # 5 minute timeout per test ] testpaths = ["tests"] pythonpath = ["src"] @@ -229,7 +275,7 @@ output = "coverage.xml" [tool.tox] legacy_tox_ini = """ [tox] -envlist = py38,py39,py310,py311,py312,lint,type +envlist = py39,py310,py311,py312,lint,type [testenv] deps = diff --git a/pytest.ini b/pytest.ini index a7e3be7..de6e781 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,4 @@ -[tool:pytest] +[pytest] # Test discovery patterns optimized for modular architecture testpaths = tests python_files = test_*.py *_test.py @@ -40,28 +40,13 @@ markers = parametrized: Parametrized tests with multiple input scenarios mock: Tests that heavily use mocking real_api: Tests that make actual API calls (requires auth) - -# Enhanced filtering and collection rules -collect_ignore = - build/ - dist/ - .tox/ - venv/ - .venv/ - env/ - .env/ - docs/_build/ - .eggs/ - *.egg-info/ - -collect_ignore_glob = - **/.* - **/__pycache__ - **/node_modules + auth: Authentication and authorization tests + resilience: Network resilience and retry tests + performance: Performance and load tests # Performance and caching optimizations cache_dir = .pytest_cache -python_paths = src +pythonpath = src filterwarnings = ignore::DeprecationWarning ignore::PendingDeprecationWarning @@ -69,11 +54,7 @@ filterwarnings = error::pytest.PytestUnraisableExceptionWarning # Test execution controls -timeout = 300 -timeout_method = thread junit_family = xunit2 -junit_logging = all -junit_log_passing_tests = false # Coverage configuration enhancements [coverage:run] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 8464c99..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,25 +0,0 @@ -# Include base requirements --r requirements.txt - -# Testing -pytest>=7.4.0 -pytest-cov>=4.1.0 -pytest-mock>=3.11.0 -psutil>=5.9.0 # For performance and memory testing - -# Code quality -black>=23.0.0 -ruff>=0.1.0 -mypy>=1.5.0 -types-click>=7.1.0 - -# Pre-commit hooks -pre-commit>=3.4.0 - -# Build and release -build>=1.0.0 -twine>=4.0.0 -tox>=4.0.0 - -# TOML parsing for older Python versions -tomli>=2.0.0;python_version<'3.11' diff --git a/scripts/ci_check.py b/scripts/ci_check.py index c40db8e..461eed4 100755 --- a/scripts/ci_check.py +++ b/scripts/ci_check.py @@ -11,12 +11,12 @@ """ import argparse +from datetime import datetime +from pathlib import Path import subprocess import sys import time -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List, Tuple +from typing import Any class Colors: @@ -40,7 +40,7 @@ def __init__(self, project_root: Path, verbose: bool = False): self.project_root = project_root self.verbose = verbose self.start_time = time.time() - self.check_results: Dict[str, Dict[str, Any]] = {} + self.check_results: dict[str, dict[str, Any]] = {} self.total_checks = 0 self.passed_checks = 0 self.failed_checks = 0 @@ -74,11 +74,11 @@ def print_info(self, message: str) -> None: def run_command( self, - cmd: List[str], + cmd: list[str], description: str, timeout: int = 300, check_output: bool = False, - ) -> Tuple[bool, str, float]: + ) -> tuple[bool, str, float]: """Run command with timing and elegant error handling.""" start_time = time.time() @@ -138,7 +138,7 @@ def validate_environment(self) -> bool: all_passed = True for cmd, desc in checks: - success, output, duration = self.run_command(cmd, desc, timeout=10) + success, output, duration = self.run_command(cmd, desc, timeout=30) if not success: all_passed = False @@ -205,7 +205,7 @@ def run_tests(self) -> bool: "--cov-report=xml:coverage.xml", "--cov-report=term-missing", ], - "Test suite execution", + "Test suite execution (1132 tests - this may take several minutes)", timeout=600, # 10 minutes for full test suite check_output=True, ) @@ -233,6 +233,40 @@ def run_tests(self) -> bool: return success + def check_trailing_whitespace(self) -> bool: + """Check for trailing whitespace.""" + self.print_step("WHITESPACE", "Checking trailing whitespace") + + success, output, duration = self.run_command( + ["pre-commit", "run", "trailing-whitespace", "--all-files"], + "Trailing whitespace check", + ) + + self.check_results["trailing_whitespace"] = { + "passed": success, + "duration": duration, + "output": output, + } + + return success + + def check_end_of_files(self) -> bool: + """Check for proper end of files.""" + self.print_step("EOF", "Checking end of files") + + success, output, duration = self.run_command( + ["pre-commit", "run", "end-of-file-fixer", "--all-files"], + "End of file check", + ) + + self.check_results["end_of_files"] = { + "passed": success, + "duration": duration, + "output": output, + } + + return success + def run_pre_commit_hooks(self) -> bool: """Run pre-commit hooks.""" self.print_step("HOOKS", "Running pre-commit hooks") @@ -363,6 +397,8 @@ def run_fast_check(self) -> bool: ("Code Formatting", self.check_code_formatting), ("Code Linting", self.check_linting), ("Type Checking", self.check_type_hints), + ("Trailing Whitespace", self.check_trailing_whitespace), + ("End of Files", self.check_end_of_files), ] self.total_checks = len(pipeline_steps) diff --git a/scripts/test_config.py b/scripts/test_config.py index 42f7a6d..039d692 100755 --- a/scripts/test_config.py +++ b/scripts/test_config.py @@ -12,11 +12,11 @@ import argparse import json +from pathlib import Path import subprocess import sys import time -from pathlib import Path -from typing import Any, Dict +from typing import Any class TestConfig: @@ -109,7 +109,7 @@ def run_performance_tests(self) -> int: ] return subprocess.call(cmd) - def analyze_test_suite(self) -> Dict[str, Any]: + def analyze_test_suite(self) -> dict[str, Any]: """Analyze test suite structure and metrics.""" print("๐Ÿ“Š Analyzing test suite...") @@ -217,7 +217,7 @@ def _get_pytest_version(self) -> str: except Exception: return "unknown" - def _print_analysis_summary(self, analysis: Dict[str, Any]) -> None: + def _print_analysis_summary(self, analysis: dict[str, Any]) -> None: """Print analysis summary to console.""" print("\n" + "=" * 60) print("๐Ÿงช TEST SUITE ANALYSIS SUMMARY") diff --git a/src/toady/cli.py b/src/toady/cli.py index 5bdeb87..fc31748 100644 --- a/src/toady/cli.py +++ b/src/toady/cli.py @@ -114,9 +114,8 @@ def main() -> None: if debug: # In debug mode, show the full traceback raise - else: - # In normal mode, show a user-friendly message - handle_error(e, show_traceback=False) + # In normal mode, show a user-friendly message + handle_error(e, show_traceback=False) if __name__ == "__main__": diff --git a/src/toady/commands/fetch.py b/src/toady/commands/fetch.py index 94f66cf..f4c28c4 100644 --- a/src/toady/commands/fetch.py +++ b/src/toady/commands/fetch.py @@ -137,7 +137,7 @@ def fetch( selected_pr_number = pr_number # Initialize with provided value for error handling try: # Create fetch service and retrieve threads using integrated PR selection - fetch_service = FetchService() + fetch_service = FetchService(output_format=output_format) threads, selected_pr_number = ( fetch_service.fetch_review_threads_with_pr_selection( pr_number=pr_number, diff --git a/src/toady/commands/reply.py b/src/toady/commands/reply.py index 5dd105d..a29669e 100644 --- a/src/toady/commands/reply.py +++ b/src/toady/commands/reply.py @@ -1,7 +1,7 @@ """Reply command implementation.""" import json -from typing import Any, Dict, Optional, Tuple +from typing import Any, Optional import click @@ -157,7 +157,7 @@ def validate_reply_target_id(reply_to_id: str) -> str: raise click.BadParameter(enhanced_msg, param_hint="--id") from e -def _validate_reply_args(reply_to_id: str, body: str) -> Tuple[str, str]: +def _validate_reply_args(reply_to_id: str, body: str) -> tuple[str, str]: """Validate reply command arguments. Args: @@ -208,7 +208,7 @@ def _validate_reply_args(reply_to_id: str, body: str) -> Tuple[str, str]: return reply_to_id, body -def _print_pretty_reply(reply_info: Dict[str, Any], verbose: bool) -> None: +def _print_pretty_reply(reply_info: dict[str, Any], verbose: bool) -> None: """Print reply information in pretty format. Args: @@ -248,8 +248,8 @@ def _print_pretty_reply(reply_info: Dict[str, Any], verbose: bool) -> None: def _build_json_reply( - id: str, reply_info: Dict[str, Any], verbose: bool -) -> Dict[str, Any]: + id: str, reply_info: dict[str, Any], verbose: bool +) -> dict[str, Any]: """Build JSON response for reply command. Args: @@ -282,7 +282,7 @@ def _build_json_reply( "review_id", ] for field in optional_fields: - if field in reply_info and reply_info[field]: + if reply_info.get(field): result[field] = reply_info[field] # Include verbose flag in output to indicate extended info diff --git a/src/toady/commands/resolve.py b/src/toady/commands/resolve.py index 9095683..6146a12 100644 --- a/src/toady/commands/resolve.py +++ b/src/toady/commands/resolve.py @@ -2,7 +2,7 @@ import json import time -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Optional import click @@ -27,7 +27,7 @@ def _fetch_and_filter_threads( pr_number: int, undo: bool, pretty: bool, limit: int -) -> List[Any]: +) -> list[Any]: """Fetch and filter threads based on resolution action. Args: @@ -62,7 +62,7 @@ def _fetch_and_filter_threads( def _handle_confirmation_prompt( ctx: click.Context, - target_threads: List[Any], + target_threads: list[Any], action: str, action_symbol: str, pr_number: int, @@ -107,12 +107,12 @@ def _handle_confirmation_prompt( def _process_threads( - target_threads: List[Any], + target_threads: list[Any], undo: bool, action_present: str, action_symbol: str, pretty: bool, -) -> Tuple[int, int, List[Dict[str, str]]]: +) -> tuple[int, int, list[dict[str, str]]]: """Process threads for resolution/unresolve with error handling. Args: @@ -173,10 +173,10 @@ def _process_threads( def _display_summary( - target_threads: List[Any], + target_threads: list[Any], succeeded: int, failed: int, - failed_threads: List[Dict[str, str]], + failed_threads: list[dict[str, str]], action: str, action_past: str, pr_number: int, @@ -216,7 +216,7 @@ def _display_summary( click.echo(json.dumps(result)) -def _get_action_labels(undo: bool) -> Tuple[str, str, str, str]: +def _get_action_labels(undo: bool) -> tuple[str, str, str, str]: """Get action labels for bulk operations. Args: @@ -227,8 +227,7 @@ def _get_action_labels(undo: bool) -> Tuple[str, str, str, str]: """ if undo: return "unresolve", "unresolved", "Unresolving", "๐Ÿ”“" - else: - return "resolve", "resolved", "Resolving", "๐Ÿ”’" + return "resolve", "resolved", "Resolving", "๐Ÿ”’" def _handle_empty_threads( @@ -454,7 +453,7 @@ def _show_single_resolve_progress(thread_id: str, undo: bool, pretty: bool) -> N def _handle_single_resolve_success( - result: Dict[str, Any], undo: bool, pretty: bool + result: dict[str, Any], undo: bool, pretty: bool ) -> None: """Handle successful single thread resolution. diff --git a/src/toady/commands/schema.py b/src/toady/commands/schema.py index 3783bf0..e7be744 100644 --- a/src/toady/commands/schema.py +++ b/src/toady/commands/schema.py @@ -1,9 +1,9 @@ """Schema command implementation.""" import json -import sys from pathlib import Path -from typing import Any, Dict, List, Optional +import sys +from typing import Any, Optional import click @@ -62,13 +62,13 @@ def validate(cache_dir: Optional[Path], force_refresh: bool, output: str) -> Non validator = GitHubSchemaValidator(cache_dir=cache_dir) except (OSError, PermissionError) as e: raise FileOperationError( - message=f"Failed to initialize schema validator: {str(e)}", + message=f"Failed to initialize schema validator: {e!s}", file_path=str(cache_dir) if cache_dir else "default cache directory", operation="initialize", ) from e except Exception as e: raise ConfigurationError( - message=f"Configuration error initializing schema validator: {str(e)}", + message=f"Configuration error initializing schema validator: {e!s}", ) from e # Fetch schema with enhanced error handling @@ -77,12 +77,12 @@ def validate(cache_dir: Optional[Path], force_refresh: bool, output: str) -> Non validator.fetch_schema(force_refresh=force_refresh) except (ConnectionError, TimeoutError) as e: raise NetworkError( - message=f"Network error fetching GitHub schema: {str(e)}", + message=f"Network error fetching GitHub schema: {e!s}", url="https://api.github.com/graphql", ) from e except (OSError, PermissionError) as e: raise FileOperationError( - message=f"File operation error during schema fetch: {str(e)}", + message=f"File operation error during schema fetch: {e!s}", file_path=str(cache_dir) if cache_dir else "default cache directory", operation="write", ) from e @@ -93,7 +93,7 @@ def validate(cache_dir: Optional[Path], force_refresh: bool, output: str) -> Non report = validator.generate_compatibility_report() except Exception as e: raise ToadyError( - message=f"Failed to generate compatibility report: {str(e)}", + message=f"Failed to generate compatibility report: {e!s}", ) from e # Output results with error handling @@ -104,7 +104,7 @@ def validate(cache_dir: Optional[Path], force_refresh: bool, output: str) -> Non _display_summary_report(report) except (TypeError, ValueError) as e: raise ToadyError( - message=f"Failed to format output: {str(e)}", + message=f"Failed to format output: {e!s}", ) from e # Set exit code based on critical errors @@ -113,7 +113,7 @@ def validate(cache_dir: Optional[Path], force_refresh: bool, output: str) -> Non sys.exit(1 if has_critical_errors else 0) except Exception as e: raise ToadyError( - message=f"Failed to analyze report for critical errors: {str(e)}", + message=f"Failed to analyze report for critical errors: {e!s}", ) from e except SchemaValidationError as e: @@ -166,13 +166,13 @@ def fetch(cache_dir: Optional[Path], force_refresh: bool) -> None: validator = GitHubSchemaValidator(cache_dir=cache_dir) except (OSError, PermissionError) as e: raise FileOperationError( - message=f"Failed to initialize schema validator: {str(e)}", + message=f"Failed to initialize schema validator: {e!s}", file_path=str(cache_dir) if cache_dir else "default cache directory", operation="initialize", ) from e except Exception as e: raise ConfigurationError( - message=f"Configuration error initializing schema validator: {str(e)}", + message=f"Configuration error initializing schema validator: {e!s}", ) from e # Fetch schema with enhanced error handling @@ -181,12 +181,12 @@ def fetch(cache_dir: Optional[Path], force_refresh: bool) -> None: schema = validator.fetch_schema(force_refresh=force_refresh) except (ConnectionError, TimeoutError) as e: raise NetworkError( - message=f"Network error fetching GitHub schema: {str(e)}", + message=f"Network error fetching GitHub schema: {e!s}", url="https://api.github.com/graphql", ) from e except (OSError, PermissionError) as e: raise FileOperationError( - message=f"File operation error during schema fetch: {str(e)}", + message=f"File operation error during schema fetch: {e!s}", file_path=str(cache_dir) if cache_dir else "default cache directory", operation="write", ) from e @@ -262,13 +262,13 @@ def check(query: str, cache_dir: Optional[Path], output: str) -> None: validator = GitHubSchemaValidator(cache_dir=cache_dir) except (OSError, PermissionError) as e: raise FileOperationError( - message=f"Failed to initialize schema validator: {str(e)}", + message=f"Failed to initialize schema validator: {e!s}", file_path=str(cache_dir) if cache_dir else "default cache directory", operation="initialize", ) from e except Exception as e: raise ConfigurationError( - message=f"Configuration error initializing schema validator: {str(e)}", + message=f"Configuration error initializing schema validator: {e!s}", ) from e # Validate query with error handling @@ -277,7 +277,7 @@ def check(query: str, cache_dir: Optional[Path], output: str) -> None: errors = validator.validate_query(query) except Exception as e: raise ToadyError( - message=f"Failed to validate query: {str(e)}", + message=f"Failed to validate query: {e!s}", context={"query_length": len(query), "query_preview": query[:100]}, ) from e @@ -289,7 +289,7 @@ def check(query: str, cache_dir: Optional[Path], output: str) -> None: _display_query_validation_results(errors) except (TypeError, ValueError) as e: raise ToadyError( - message=f"Failed to format validation results: {str(e)}", + message=f"Failed to format validation results: {e!s}", ) from e # Set exit code based on critical errors @@ -298,7 +298,7 @@ def check(query: str, cache_dir: Optional[Path], output: str) -> None: sys.exit(1 if critical_errors else 0) except Exception as e: raise ToadyError( - message=f"Failed to analyze validation errors: {str(e)}", + message=f"Failed to analyze validation errors: {e!s}", ) from e except SchemaValidationError as e: @@ -319,7 +319,7 @@ def check(query: str, cache_dir: Optional[Path], output: str) -> None: sys.exit(1) -def _display_summary_report(report: Dict[str, Any]) -> None: +def _display_summary_report(report: dict[str, Any]) -> None: """Display a human-readable summary of the validation report. Raises: @@ -345,7 +345,7 @@ def _display_summary_report(report: Dict[str, Any]) -> None: except Exception as e: raise ToadyError( - message=f"Failed to display summary report header: {str(e)}", + message=f"Failed to display summary report header: {e!s}", context={ "report_keys": ( list(report.keys()) if isinstance(report, dict) else "not_dict" @@ -401,7 +401,7 @@ def _display_summary_report(report: Dict[str, Any]) -> None: click.echo("\n" + "=" * 60) -def _display_query_validation_results(errors: List[Dict[str, Any]]) -> None: +def _display_query_validation_results(errors: list[dict[str, Any]]) -> None: """Display validation results for a single query. Raises: @@ -436,7 +436,7 @@ def _display_query_validation_results(errors: List[Dict[str, Any]]) -> None: except Exception as e: raise ToadyError( - message=f"Failed to process validation results: {str(e)}", + message=f"Failed to process validation results: {e!s}", context={ "errors_count": len(errors) if isinstance(errors, list) else "not_list" }, @@ -459,7 +459,7 @@ def _display_query_validation_results(errors: List[Dict[str, Any]]) -> None: click.echo(f" Path: {warning['path']}") -def _has_critical_errors(report: Dict[str, Any]) -> bool: +def _has_critical_errors(report: dict[str, Any]) -> bool: """Check if the report contains any critical errors. Raises: @@ -500,7 +500,7 @@ def _has_critical_errors(report: Dict[str, Any]) -> bool: except Exception as e: raise ToadyError( - message=f"Failed to analyze report for critical errors: {str(e)}", + message=f"Failed to analyze report for critical errors: {e!s}", context={ "report_keys": ( list(report.keys()) if isinstance(report, dict) else "not_dict" diff --git a/src/toady/error_handling.py b/src/toady/error_handling.py index dc289bc..8a81ec3 100644 --- a/src/toady/error_handling.py +++ b/src/toady/error_handling.py @@ -5,9 +5,10 @@ with helpful guidance for resolution. """ -import sys from enum import IntEnum -from typing import Any, Dict, List, Optional +import os +import sys +from typing import Any, Optional from .exceptions import ( CommandExecutionError, @@ -312,9 +313,14 @@ def format_error(error: Exception) -> str: return message # For unexpected errors, use the generic template - return ( - ErrorMessageTemplates.GENERIC_ERROR + f"\n\n๐Ÿ” Error details: {str(error)}" - ) + message = ErrorMessageTemplates.GENERIC_ERROR + + # Only show raw error details in debug mode + debug = os.environ.get("TOADY_DEBUG", "").lower() in ("1", "true", "yes") + if debug: + message += f"\n\n๐Ÿ” Error details: {error!s}" + + return message @staticmethod def get_exit_code(error: Exception) -> int: @@ -385,8 +391,8 @@ def handle_error(error: Exception, show_traceback: bool = False) -> None: def create_user_friendly_error( message: str, - suggestions: Optional[List[str]] = None, - context: Optional[Dict[str, Any]] = None, + suggestions: Optional[list[str]] = None, + context: Optional[dict[str, Any]] = None, ) -> str: """Create a user-friendly error message. diff --git a/src/toady/exceptions.py b/src/toady/exceptions.py index 1868319..c34ccaf 100644 --- a/src/toady/exceptions.py +++ b/src/toady/exceptions.py @@ -5,7 +5,7 @@ """ from enum import Enum -from typing import Any, Dict, List, Optional +from typing import Any, Optional class ErrorSeverity(Enum): @@ -81,8 +81,8 @@ def __init__( message: str, error_code: ErrorCode = ErrorCode.UNKNOWN_ERROR, severity: ErrorSeverity = ErrorSeverity.MEDIUM, - context: Optional[Dict[str, Any]] = None, - suggestions: Optional[List[str]] = None, + context: Optional[dict[str, Any]] = None, + suggestions: Optional[list[str]] = None, ) -> None: """Initialize a ToadyError. @@ -104,7 +104,7 @@ def __str__(self) -> str: """Return a string representation of the error.""" return f"[{self.error_code.name}] {self.message}" - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the error to a dictionary representation. Returns: diff --git a/src/toady/formatters/format_interfaces.py b/src/toady/formatters/format_interfaces.py index 0b429c2..a3dcc49 100644 --- a/src/toady/formatters/format_interfaces.py +++ b/src/toady/formatters/format_interfaces.py @@ -6,7 +6,7 @@ """ from abc import ABC, abstractmethod -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Optional, Union from ..models.models import Comment, ReviewThread @@ -19,7 +19,7 @@ class IFormatter(ABC): """ @abstractmethod - def format_threads(self, threads: List[ReviewThread]) -> str: + def format_threads(self, threads: list[ReviewThread]) -> str: """Format a list of review threads. Args: @@ -28,10 +28,9 @@ def format_threads(self, threads: List[ReviewThread]) -> str: Returns: Formatted string representation of the threads. """ - pass @abstractmethod - def format_comments(self, comments: List[Comment]) -> str: + def format_comments(self, comments: list[Comment]) -> str: """Format a list of comments. Args: @@ -40,7 +39,6 @@ def format_comments(self, comments: List[Comment]) -> str: Returns: Formatted string representation of the comments. """ - pass @abstractmethod def format_object(self, obj: Any) -> str: @@ -52,10 +50,9 @@ def format_object(self, obj: Any) -> str: Returns: Formatted string representation of the object. """ - pass @abstractmethod - def format_array(self, items: List[Any]) -> str: + def format_array(self, items: list[Any]) -> str: """Format an array of items. Args: @@ -64,10 +61,9 @@ def format_array(self, items: List[Any]) -> str: Returns: Formatted string representation of the array. """ - pass @abstractmethod - def format_primitive(self, value: Union[str, int, float, bool, None]) -> str: + def format_primitive(self, value: Union[str, float, bool, None]) -> str: """Format a primitive value. Args: @@ -76,10 +72,9 @@ def format_primitive(self, value: Union[str, int, float, bool, None]) -> str: Returns: Formatted string representation of the value. """ - pass @abstractmethod - def format_error(self, error: Dict[str, Any]) -> str: + def format_error(self, error: dict[str, Any]) -> str: """Format an error object. Args: @@ -88,10 +83,9 @@ def format_error(self, error: Dict[str, Any]) -> str: Returns: Formatted string representation of the error. """ - pass def format_success_message( - self, message: str, details: Optional[Dict[str, Any]] = None + self, message: str, details: Optional[dict[str, Any]] = None ) -> str: """Format a success message. @@ -109,7 +103,7 @@ def format_success_message( return self.format_object({"success": True, "message": message}) def format_warning_message( - self, message: str, details: Optional[Dict[str, Any]] = None + self, message: str, details: Optional[dict[str, Any]] = None ) -> str: """Format a warning message. @@ -151,23 +145,25 @@ def _safe_serialize(self, obj: Any) -> Any: Returns: Serializable representation of the object. """ - if hasattr(obj, "to_dict"): - return obj.to_dict() - elif isinstance(obj, (list, tuple)): + if hasattr(obj, "to_dict") and callable(obj.to_dict): + try: + return obj.to_dict() + except Exception: + return str(obj) + if isinstance(obj, (list, tuple)): return [self._safe_serialize(item) for item in obj] - elif isinstance(obj, dict): + if isinstance(obj, dict): return {key: self._safe_serialize(value) for key, value in obj.items()} - elif hasattr(obj, "__dict__"): + if hasattr(obj, "__dict__"): return self._safe_serialize(obj.__dict__) - else: - # For primitive types and other serializable objects - try: - import json + # For primitive types and other serializable objects + try: + import json - json.dumps(obj) # Test if it's JSON serializable - return obj - except (TypeError, ValueError): - return str(obj) + json.dumps(obj) # Test if it's JSON serializable + return obj + except (TypeError, ValueError): + return str(obj) def _handle_empty_data( self, data: Any, empty_message: str = "No data available." @@ -187,7 +183,7 @@ def _handle_empty_data( return empty_message return None - def format_comments(self, comments: List[Comment]) -> str: + def format_comments(self, comments: list[Comment]) -> str: """Default implementation for formatting comments. Args: @@ -200,7 +196,7 @@ def format_comments(self, comments: List[Comment]) -> str: return self.format_array([comment.to_dict() for comment in comments]) def format_success_message( - self, message: str, details: Optional[Dict[str, Any]] = None + self, message: str, details: Optional[dict[str, Any]] = None ) -> str: """Format a success message (implementation in base class).""" success_data = {"success": True, "message": message} @@ -209,7 +205,7 @@ def format_success_message( return self.format_object(success_data) def format_warning_message( - self, message: str, details: Optional[Dict[str, Any]] = None + self, message: str, details: Optional[dict[str, Any]] = None ) -> str: """Format a warning message (implementation in base class).""" warning_data = {"warning": True, "message": message} @@ -242,7 +238,7 @@ def __init__( indent: int = 2, sort_keys: bool = False, ensure_ascii: bool = False, - separators: Optional[Tuple[str, str]] = None, + separators: Optional[tuple[str, str]] = None, **kwargs: Any, ) -> None: """Initialize formatter options. @@ -260,7 +256,7 @@ def __init__( self.separators = separators self.extra_options = kwargs - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert options to dictionary. Returns: @@ -278,7 +274,7 @@ def to_dict(self) -> Dict[str, Any]: class FormatterFactory: """Factory for creating formatter instances.""" - _formatters: Dict[str, type] = {} + _formatters: dict[str, type] = {} @classmethod def register(cls, name: str, formatter_class: type) -> None: @@ -316,11 +312,11 @@ def create(cls, name: str, **options: Any) -> IFormatter: return formatter_instance # type: ignore[no-any-return] except Exception as e: raise FormatterError( - f"Failed to create formatter '{name}': {str(e)}", original_error=e + f"Failed to create formatter '{name}': {e!s}", original_error=e ) from e @classmethod - def list_formatters(cls) -> List[str]: + def list_formatters(cls) -> list[str]: """List available formatter names. Returns: diff --git a/src/toady/formatters/format_selection.py b/src/toady/formatters/format_selection.py index 2eb02cf..60ffcc6 100644 --- a/src/toady/formatters/format_selection.py +++ b/src/toady/formatters/format_selection.py @@ -5,7 +5,7 @@ """ import os -from typing import Any, Callable, Dict, List, Optional, TypeVar, cast +from typing import Any, Callable, Optional, TypeVar, cast import click @@ -102,7 +102,7 @@ def format_warning_message( class FormatSelectionError(Exception): """Exception raised when format selection fails.""" - def __init__(self, message: str, available_formats: Optional[List[str]] = None): + def __init__(self, message: str, available_formats: Optional[list[str]] = None): """Initialize format selection error. Args: @@ -227,7 +227,7 @@ def create_format_option(**kwargs: Any) -> Callable[[F], F]: } default_kwargs.update(kwargs) - return cast(Callable[[F], F], click.option("--format", **default_kwargs)) # type: ignore[call-overload] + return cast("Callable[[F], F]", click.option("--format", **default_kwargs)) # type: ignore[call-overload] def create_legacy_pretty_option(**kwargs: Any) -> Callable[[F], F]: @@ -248,7 +248,7 @@ def create_legacy_pretty_option(**kwargs: Any) -> Callable[[F], F]: } default_kwargs.update(kwargs) - return cast(Callable[[F], F], click.option("--pretty", **default_kwargs)) # type: ignore[call-overload] + return cast("Callable[[F], F]", click.option("--pretty", **default_kwargs)) # type: ignore[call-overload] # Format-specific output functions @@ -299,7 +299,7 @@ def format_object_output(obj: Any, format_name: str) -> None: def format_success_message( - message: str, format_name: str, details: Optional[Dict[str, Any]] = None + message: str, format_name: str, details: Optional[dict[str, Any]] = None ) -> None: """Format and output a success message. @@ -322,7 +322,7 @@ def format_success_message( click.echo(output) -def format_error_message(error: Dict[str, Any], format_name: str) -> None: +def format_error_message(error: dict[str, Any], format_name: str) -> None: """Format and output an error message. Args: diff --git a/src/toady/formatters/formatters.py b/src/toady/formatters/formatters.py index 15f15af..f09ab8f 100644 --- a/src/toady/formatters/formatters.py +++ b/src/toady/formatters/formatters.py @@ -6,7 +6,7 @@ import json import textwrap -from typing import List, Optional +from typing import Optional import click @@ -19,7 +19,7 @@ class OutputFormatter: """Base class for output formatters.""" @staticmethod - def format_threads(threads: List[ReviewThread], pretty: bool = False) -> str: + def format_threads(threads: list[ReviewThread], pretty: bool = False) -> str: """Format a list of review threads for output. Args: @@ -31,15 +31,14 @@ def format_threads(threads: List[ReviewThread], pretty: bool = False) -> str: """ if pretty: return PrettyFormatter.format_threads(threads) - else: - return JSONFormatter.format_threads(threads) + return JSONFormatter.format_threads(threads) class JSONFormatter: """JSON output formatter.""" @staticmethod - def format_threads(threads: List[ReviewThread]) -> str: + def format_threads(threads: list[ReviewThread]) -> str: """Format threads as JSON array. Args: @@ -185,7 +184,7 @@ def _format_comment( return "\n".join(lines) @staticmethod - def format_threads(threads: List[ReviewThread]) -> str: + def format_threads(threads: list[ReviewThread]) -> str: """Format threads in a human-readable format with full comment content. Args: @@ -308,7 +307,7 @@ def format_result_summary(count: int, thread_type: str) -> str: def format_fetch_output( - threads: List[ReviewThread], + threads: list[ReviewThread], pretty: bool = False, show_progress: bool = True, pr_number: Optional[int] = None, diff --git a/src/toady/formatters/json_formatter.py b/src/toady/formatters/json_formatter.py index dbf23a4..889c9f7 100644 --- a/src/toady/formatters/json_formatter.py +++ b/src/toady/formatters/json_formatter.py @@ -6,7 +6,7 @@ """ import json -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union from ..models.models import Comment, ReviewThread from .format_interfaces import BaseFormatter, FormatterError, FormatterOptions @@ -32,7 +32,7 @@ def __init__( self.formatter_options = options or FormatterOptions(**kwargs) # Extract JSON-specific options - self.json_options: Dict[str, Any] = { + self.json_options: dict[str, Any] = { "indent": self.formatter_options.indent, "sort_keys": self.formatter_options.sort_keys, "ensure_ascii": self.formatter_options.ensure_ascii, @@ -41,7 +41,7 @@ def __init__( if self.formatter_options.separators: self.json_options["separators"] = self.formatter_options.separators - def format_threads(self, threads: List[ReviewThread]) -> str: + def format_threads(self, threads: list[ReviewThread]) -> str: """Format a list of review threads as JSON. Args: @@ -70,7 +70,7 @@ def format_threads(self, threads: List[ReviewThread]) -> str: except Exception as e: thread_id = getattr(thread, "thread_id", "unknown") raise FormatterError( - f"Failed to serialize thread {thread_id}: {str(e)}", + f"Failed to serialize thread {thread_id}: {e!s}", original_error=e, ) from e @@ -80,10 +80,10 @@ def format_threads(self, threads: List[ReviewThread]) -> str: if isinstance(e, FormatterError): raise raise FormatterError( - f"Failed to format threads as JSON: {str(e)}", original_error=e + f"Failed to format threads as JSON: {e!s}", original_error=e ) from e - def format_comments(self, comments: List[Comment]) -> str: + def format_comments(self, comments: list[Comment]) -> str: """Format a list of comments as JSON. Args: @@ -112,7 +112,7 @@ def format_comments(self, comments: List[Comment]) -> str: except Exception as e: comment_id = getattr(comment, "comment_id", "unknown") raise FormatterError( - f"Failed to serialize comment {comment_id}: {str(e)}", + f"Failed to serialize comment {comment_id}: {e!s}", original_error=e, ) from e @@ -122,7 +122,7 @@ def format_comments(self, comments: List[Comment]) -> str: if isinstance(e, FormatterError): raise raise FormatterError( - f"Failed to format comments as JSON: {str(e)}", original_error=e + f"Failed to format comments as JSON: {e!s}", original_error=e ) from e def format_object(self, obj: Any) -> str: @@ -142,10 +142,10 @@ def format_object(self, obj: Any) -> str: return json.dumps(serializable_obj, **self.json_options) except Exception as e: raise FormatterError( - f"Failed to format object as JSON: {str(e)}", original_error=e + f"Failed to format object as JSON: {e!s}", original_error=e ) from e - def format_array(self, items: List[Any]) -> str: + def format_array(self, items: list[Any]) -> str: """Format an array of items as JSON. Args: @@ -173,7 +173,7 @@ def format_array(self, items: List[Any]) -> str: serializable_items.append(serializable_item) except Exception as e: raise FormatterError( - f"Failed to serialize array item at index {i}: {str(e)}", + f"Failed to serialize array item at index {i}: {e!s}", original_error=e, ) from e @@ -183,10 +183,10 @@ def format_array(self, items: List[Any]) -> str: if isinstance(e, FormatterError): raise raise FormatterError( - f"Failed to format array as JSON: {str(e)}", original_error=e + f"Failed to format array as JSON: {e!s}", original_error=e ) from e - def format_primitive(self, value: Union[str, int, float, bool, None]) -> str: + def format_primitive(self, value: Union[str, float, bool, None]) -> str: """Format a primitive value as JSON. Args: @@ -202,10 +202,10 @@ def format_primitive(self, value: Union[str, int, float, bool, None]) -> str: return json.dumps(value, **self.json_options) except Exception as e: raise FormatterError( - f"Failed to format primitive value as JSON: {str(e)}", original_error=e + f"Failed to format primitive value as JSON: {e!s}", original_error=e ) from e - def format_error(self, error: Dict[str, Any]) -> str: + def format_error(self, error: dict[str, Any]) -> str: """Format an error object as JSON. Args: @@ -228,11 +228,11 @@ def format_error(self, error: Dict[str, Any]) -> str: return json.dumps(error_dict, **self.json_options) except Exception as e: raise FormatterError( - f"Failed to format error as JSON: {str(e)}", original_error=e + f"Failed to format error as JSON: {e!s}", original_error=e ) from e def format_success_message( - self, message: str, details: Optional[Dict[str, Any]] = None + self, message: str, details: Optional[dict[str, Any]] = None ) -> str: """Format a success message as JSON. @@ -251,7 +251,7 @@ def format_success_message( return self.format_object(success_data) def format_warning_message( - self, message: str, details: Optional[Dict[str, Any]] = None + self, message: str, details: Optional[dict[str, Any]] = None ) -> str: """Format a warning message as JSON. @@ -270,7 +270,7 @@ def format_warning_message( return self.format_object(warning_data) def format_reply_result( - self, reply_info: Dict[str, Any], verbose: bool = False + self, reply_info: dict[str, Any], verbose: bool = False ) -> str: """Format reply command result as JSON. @@ -302,7 +302,7 @@ def format_reply_result( ] for field in optional_fields: - if field in reply_info and reply_info[field]: + if reply_info.get(field): result[field] = reply_info[field] # Include verbose flag in output to indicate extended info @@ -311,7 +311,7 @@ def format_reply_result( return self.format_object(result) - def format_resolve_result(self, resolve_info: Dict[str, Any]) -> str: + def format_resolve_result(self, resolve_info: dict[str, Any]) -> str: """Format resolve command result as JSON. Args: @@ -352,7 +352,7 @@ def _safe_serialize(self, obj: Any) -> Any: return obj.to_dict() except Exception: # Fallback to string representation - return f"<{type(obj).__name__}: {str(obj)}>" + return f"<{type(obj).__name__}: {obj!s}>" # Handle dictionaries if isinstance(obj, dict): @@ -389,7 +389,7 @@ def _safe_serialize(self, obj: Any) -> Any: default_json_formatter = JSONFormatter() -def format_threads_json(threads: List[ReviewThread]) -> str: +def format_threads_json(threads: list[ReviewThread]) -> str: """Convenience function for formatting threads as JSON. Args: @@ -401,7 +401,7 @@ def format_threads_json(threads: List[ReviewThread]) -> str: return default_json_formatter.format_threads(threads) -def format_comments_json(comments: List[Comment]) -> str: +def format_comments_json(comments: list[Comment]) -> str: """Convenience function for formatting comments as JSON. Args: diff --git a/src/toady/formatters/pretty_formatter.py b/src/toady/formatters/pretty_formatter.py index 9531db6..91d98a6 100644 --- a/src/toady/formatters/pretty_formatter.py +++ b/src/toady/formatters/pretty_formatter.py @@ -7,7 +7,7 @@ import re import textwrap -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import click @@ -40,7 +40,7 @@ def __init__( self.text_width = kwargs.get("text_width", 76) self.indent = kwargs.get("indent", " ") - def format_threads(self, threads: List[ReviewThread]) -> str: + def format_threads(self, threads: list[ReviewThread]) -> str: """Format a list of review threads in pretty format. Args: @@ -131,10 +131,10 @@ def format_threads(self, threads: List[ReviewThread]) -> str: except Exception as e: raise FormatterError( - f"Failed to format threads in pretty format: {str(e)}", original_error=e + f"Failed to format threads in pretty format: {e!s}", original_error=e ) from e - def format_comments(self, comments: List[Comment]) -> str: + def format_comments(self, comments: list[Comment]) -> str: """Format a list of comments in pretty format. Args: @@ -168,7 +168,7 @@ def format_comments(self, comments: List[Comment]) -> str: except Exception as e: raise FormatterError( - f"Failed to format comments in pretty format: {str(e)}", + f"Failed to format comments in pretty format: {e!s}", original_error=e, ) from e @@ -208,17 +208,16 @@ def format_object(self, obj: Any) -> str: serialized = self._safe_serialize(obj) if isinstance(serialized, dict): return self._format_dict(serialized) - else: - return str(serialized) + return str(serialized) except Exception: return self._style(f"<{type(obj).__name__}>", "dim") except Exception as e: raise FormatterError( - f"Failed to format object in pretty format: {str(e)}", original_error=e + f"Failed to format object in pretty format: {e!s}", original_error=e ) from e - def format_array(self, items: List[Any]) -> str: + def format_array(self, items: list[Any]) -> str: """Format an array of items in pretty format. Args: @@ -250,10 +249,10 @@ def format_array(self, items: List[Any]) -> str: except Exception as e: raise FormatterError( - f"Failed to format array in pretty format: {str(e)}", original_error=e + f"Failed to format array in pretty format: {e!s}", original_error=e ) from e - def format_primitive(self, value: Union[str, int, float, bool, None]) -> str: + def format_primitive(self, value: Union[str, float, bool, None]) -> str: """Format a primitive value in pretty format. Args: @@ -282,11 +281,11 @@ def format_primitive(self, value: Union[str, int, float, bool, None]) -> str: except Exception as e: raise FormatterError( - f"Failed to format primitive value in pretty format: {str(e)}", + f"Failed to format primitive value in pretty format: {e!s}", original_error=e, ) from e - def format_error(self, error: Dict[str, Any]) -> str: + def format_error(self, error: dict[str, Any]) -> str: """Format an error object in pretty format. Args: @@ -323,7 +322,7 @@ def format_error(self, error: Dict[str, Any]) -> str: except Exception as e: raise FormatterError( - f"Failed to format error in pretty format: {str(e)}", original_error=e + f"Failed to format error in pretty format: {e!s}", original_error=e ) from e def _style(self, text: str, color: str, bold: bool = False) -> str: @@ -413,10 +412,9 @@ def _get_status_emoji(self, status: str, is_outdated: bool = False) -> str: """ if is_outdated or status.upper() == "OUTDATED": return "โฐ" - elif status.upper() == "RESOLVED": + if status.upper() == "RESOLVED": return "โœ…" - else: - return "โŒ" + return "โŒ" def _format_file_context(self, thread: ReviewThread) -> str: """Format file context information for a thread. @@ -499,7 +497,7 @@ def _format_comment(self, comment: Comment, is_first: bool = False) -> str: return "\n".join(lines) - def _wrap_comment_content(self, content: str) -> List[str]: + def _wrap_comment_content(self, content: str) -> list[str]: """Wrap comment content with proper formatting. Args: @@ -536,7 +534,7 @@ def _wrap_comment_content(self, content: str) -> List[str]: return lines - def _format_dict(self, obj: Dict[str, Any]) -> str: + def _format_dict(self, obj: dict[str, Any]) -> str: """Format a dictionary in pretty format. Args: @@ -560,7 +558,7 @@ def _format_dict(self, obj: Dict[str, Any]) -> str: lines.append("}") return "\n".join(lines) - def _format_table(self, items: List[Dict[str, Any]]) -> str: + def _format_table(self, items: list[dict[str, Any]]) -> str: """Format a list of dictionaries as a table. Args: @@ -634,7 +632,7 @@ def _format_table(self, items: List[Dict[str, Any]]) -> str: return "\n".join(lines) - def _format_summary(self, threads: List[ReviewThread]) -> str: + def _format_summary(self, threads: list[ReviewThread]) -> str: """Format summary footer for threads. Args: @@ -675,7 +673,7 @@ def _format_summary(self, threads: List[ReviewThread]) -> str: default_pretty_formatter = PrettyFormatter() -def format_threads_pretty(threads: List[ReviewThread]) -> str: +def format_threads_pretty(threads: list[ReviewThread]) -> str: """Convenience function for formatting threads in pretty format. Args: @@ -687,7 +685,7 @@ def format_threads_pretty(threads: List[ReviewThread]) -> str: return default_pretty_formatter.format_threads(threads) -def format_comments_pretty(comments: List[Comment]) -> str: +def format_comments_pretty(comments: list[Comment]) -> str: """Convenience function for formatting comments in pretty format. Args: diff --git a/src/toady/models/models.py b/src/toady/models/models.py index 524e78f..ab7bb19 100644 --- a/src/toady/models/models.py +++ b/src/toady/models/models.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Optional from ..exceptions import ValidationError, create_validation_error from ..utils import parse_datetime @@ -31,7 +31,7 @@ def _parse_datetime(date_str: str) -> datetime: field_name="date_str", invalid_value=date_str, expected_format="ISO datetime string", - message=f"Failed to parse datetime: {str(e)}", + message=f"Failed to parse datetime: {e!s}", ) from e @@ -62,7 +62,7 @@ class ReviewThread: updated_at: datetime status: str author: str - comments: List["Comment"] = field(default_factory=list) + comments: list["Comment"] = field(default_factory=list) file_path: Optional[str] = None line: Optional[int] = None original_line: Optional[int] = None @@ -192,10 +192,10 @@ def __post_init__(self) -> None: field_name="ReviewThread", invalid_value="validation failure", expected_format="valid ReviewThread object", - message=f"Unexpected error during ReviewThread validation: {str(e)}", + message=f"Unexpected error during ReviewThread validation: {e!s}", ) from e - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the ReviewThread to a dictionary for serialization. Returns: @@ -222,7 +222,7 @@ def to_dict(self) -> Dict[str, Any]: } @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ReviewThread": + def from_dict(cls, data: dict[str, Any]) -> "ReviewThread": """Create a ReviewThread from a dictionary. Args: @@ -464,10 +464,10 @@ def __post_init__(self) -> None: field_name="Comment", invalid_value="validation failure", expected_format="valid Comment object", - message=f"Unexpected error during Comment validation: {str(e)}", + message=f"Unexpected error during Comment validation: {e!s}", ) from e - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the Comment to a dictionary for serialization. Returns: @@ -495,11 +495,11 @@ def to_dict(self) -> Dict[str, Any]: field_name="Comment", invalid_value="serialization failure", expected_format="serializable Comment object", - message=f"Failed to serialize Comment to dictionary: {str(e)}", + message=f"Failed to serialize Comment to dictionary: {e!s}", ) from e @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "Comment": + def from_dict(cls, data: dict[str, Any]) -> "Comment": """Create a Comment from a dictionary. Args: @@ -551,7 +551,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Comment": field_name="created_at", invalid_value=data.get("created_at", "missing"), expected_format="ISO datetime string", - message=f"Invalid date format for created_at: {str(err)}", + message=f"Invalid date format for created_at: {err!s}", ) from err try: @@ -561,7 +561,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Comment": field_name="updated_at", invalid_value=data.get("updated_at", "missing"), expected_format="ISO datetime string", - message=f"Invalid date format for updated_at: {str(err)}", + message=f"Invalid date format for updated_at: {err!s}", ) from err # Create instance with proper error handling @@ -587,7 +587,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Comment": field_name="Comment", invalid_value="construction failure", expected_format="valid Comment object", - message=f"Failed to create Comment from dictionary: {str(e)}", + message=f"Failed to create Comment from dictionary: {e!s}", ) from e except ValidationError: @@ -598,7 +598,7 @@ def from_dict(cls, data: Dict[str, Any]) -> "Comment": field_name="data", invalid_value=str(type(data)), expected_format="valid dictionary for Comment creation", - message=f"Unexpected error creating Comment from dictionary: {str(e)}", + message=f"Unexpected error creating Comment from dictionary: {e!s}", ) from e def __str__(self) -> str: @@ -799,10 +799,10 @@ def __post_init__(self) -> None: field_name="PullRequest", invalid_value="validation failure", expected_format="valid PullRequest object", - message=f"Unexpected error during PullRequest validation: {str(e)}", + message=f"Unexpected error during PullRequest validation: {e!s}", ) from e - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Convert the PullRequest to a dictionary for serialization. Returns: @@ -823,7 +823,7 @@ def to_dict(self) -> Dict[str, Any]: } @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "PullRequest": + def from_dict(cls, data: dict[str, Any]) -> "PullRequest": """Create a PullRequest from a dictionary. Args: diff --git a/src/toady/parsers/graphql_parser.py b/src/toady/parsers/graphql_parser.py index 4ca5668..c64d36e 100644 --- a/src/toady/parsers/graphql_parser.py +++ b/src/toady/parsers/graphql_parser.py @@ -4,9 +4,9 @@ to extract fields, arguments, and structure for validation. """ -import re from dataclasses import dataclass, field -from typing import Any, Dict, List, Optional, Set, Tuple +import re +from typing import Any, Optional @dataclass @@ -15,8 +15,8 @@ class GraphQLField: name: str alias: Optional[str] = None - arguments: Dict[str, Any] = field(default_factory=dict) - selections: List["GraphQLField"] = field(default_factory=list) + arguments: dict[str, Any] = field(default_factory=dict) + selections: list["GraphQLField"] = field(default_factory=list) parent_type: Optional[str] = None @@ -26,8 +26,8 @@ class GraphQLOperation: type: str # "query" or "mutation" name: Optional[str] = None - variables: Dict[str, str] = field(default_factory=dict) - selections: List[GraphQLField] = field(default_factory=list) + variables: dict[str, str] = field(default_factory=dict) + selections: list[GraphQLField] = field(default_factory=list) class GraphQLParser: @@ -35,7 +35,7 @@ class GraphQLParser: def __init__(self) -> None: """Initialize the parser.""" - self._type_stack: List[str] = [] + self._type_stack: list[str] = [] def parse(self, query: str) -> GraphQLOperation: """Parse a GraphQL query string. @@ -110,7 +110,7 @@ def _parse_operation(self, query: str) -> GraphQLOperation: selections=selections, ) - def _parse_variables(self, variables_str: str) -> Dict[str, str]: + def _parse_variables(self, variables_str: str) -> dict[str, str]: """Parse variable declarations.""" variables = {} @@ -128,7 +128,7 @@ def _parse_variables(self, variables_str: str) -> Dict[str, str]: def _parse_selections( self, selection_str: str, parent_type: str - ) -> List[GraphQLField]: + ) -> list[GraphQLField]: """Parse a selection set.""" selections = [] current_pos = 0 @@ -155,7 +155,7 @@ def _parse_selections( def _parse_field( self, selection_str: str, start_pos: int, parent_type: str - ) -> Tuple[Optional[GraphQLField], int]: + ) -> tuple[Optional[GraphQLField], int]: """Parse a single field from the selection set.""" # Skip whitespace while start_pos < len(selection_str) and selection_str[start_pos].isspace(): @@ -218,7 +218,7 @@ def _parse_field( def _parse_inline_fragment( self, selection_str: str, start_pos: int, parent_type: str - ) -> Tuple[Optional[GraphQLField], int]: + ) -> tuple[Optional[GraphQLField], int]: """Parse an inline fragment (... on Type).""" # Pattern for inline fragment: ... on TypeName { selections } fragment_pattern = r"\.\.\.\s+on\s+(\w+)\s*{" @@ -261,7 +261,7 @@ def _parse_inline_fragment( return field, selection_end + 1 - def _parse_arguments(self, args_str: str) -> Dict[str, Any]: + def _parse_arguments(self, args_str: str) -> dict[str, Any]: """Parse field arguments.""" arguments = {} @@ -327,7 +327,7 @@ def _find_matching_brace(self, text: str, start_pos: int, offset: int = 0) -> in return -1 - def extract_all_fields(self, operation: GraphQLOperation) -> Set[str]: + def extract_all_fields(self, operation: GraphQLOperation) -> set[str]: """Extract all field names from an operation. Args: @@ -338,7 +338,7 @@ def extract_all_fields(self, operation: GraphQLOperation) -> Set[str]: """ fields = set() - def collect_fields(selections: List[GraphQLField]) -> None: + def collect_fields(selections: list[GraphQLField]) -> None: for selection in selections: fields.add(selection.name) if selection.selections: @@ -347,7 +347,7 @@ def collect_fields(selections: List[GraphQLField]) -> None: collect_fields(operation.selections) return fields - def extract_field_paths(self, operation: GraphQLOperation) -> List[str]: + def extract_field_paths(self, operation: GraphQLOperation) -> list[str]: """Extract all field paths from an operation. Args: @@ -359,7 +359,7 @@ def extract_field_paths(self, operation: GraphQLOperation) -> List[str]: paths = [] def collect_paths( - selections: List[GraphQLField], parent_path: str = "" + selections: list[GraphQLField], parent_path: str = "" ) -> None: for selection in selections: current_path = ( diff --git a/src/toady/parsers/graphql_queries.py b/src/toady/parsers/graphql_queries.py index 5541741..1c29162 100644 --- a/src/toady/parsers/graphql_queries.py +++ b/src/toady/parsers/graphql_queries.py @@ -2,7 +2,7 @@ import base64 import re -from typing import Any, Dict, Optional +from typing import Any, Optional def _validate_cursor(cursor: str) -> bool: @@ -21,7 +21,7 @@ def _validate_cursor(cursor: str) -> bool: ValueError: If the cursor is invalid or potentially unsafe """ if not cursor: - return False + raise ValueError("Cursor cannot be empty") # Additional length check to prevent excessively long cursors (check first) if len(cursor) > 1000: @@ -38,7 +38,7 @@ def _validate_cursor(cursor: str) -> bool: try: base64.b64decode(cursor, validate=True) except Exception as e: - raise ValueError(f"Invalid Base64 cursor: {str(e)}") from e + raise ValueError(f"Invalid Base64 cursor: {e!s}") from e return True @@ -163,7 +163,7 @@ def build_query(self) -> str: return query.strip() - def build_variables(self, owner: str, repo: str, pr_number: int) -> Dict[str, Any]: + def build_variables(self, owner: str, repo: str, pr_number: int) -> dict[str, Any]: """Build the GraphQL query variables. Args: @@ -278,7 +278,7 @@ def create_paginated_query(limit: int = 100, after_cursor: Optional[str] = None) def create_paginated_query_variables( owner: str, repo: str, pr_number: int, after_cursor: Optional[str] = None -) -> Dict[str, Any]: +) -> dict[str, Any]: """Create variables for the paginated GraphQL query. Args: @@ -388,7 +388,7 @@ def build_query(self) -> str: """ return query.strip() - def build_variables(self, owner: str, repo: str) -> Dict[str, Any]: + def build_variables(self, owner: str, repo: str) -> dict[str, Any]: """Build the GraphQL query variables. Args: diff --git a/src/toady/parsers/parsers.py b/src/toady/parsers/parsers.py index 85dbce7..d545cf8 100644 --- a/src/toady/parsers/parsers.py +++ b/src/toady/parsers/parsers.py @@ -1,6 +1,6 @@ """Parsers for transforming GitHub API responses to model objects.""" -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Optional from ..exceptions import ( ValidationError, @@ -16,11 +16,10 @@ class GraphQLResponseParser: def __init__(self) -> None: """Initialize the parser.""" - pass def parse_review_threads_response( - self, response: Dict[str, Any] - ) -> List[ReviewThread]: + self, response: dict[str, Any] + ) -> list[ReviewThread]: """Parse a GraphQL response containing review threads. Args: @@ -60,7 +59,7 @@ def parse_review_threads_response( field_name=f"reviewThreads.nodes[{i}]", invalid_value=thread_data.get("id", "unknown"), expected_format="valid thread object", - message=f"Failed to parse thread at index {i}: {str(e)}", + message=f"Failed to parse thread at index {i}: {e!s}", ) from e return threads @@ -77,10 +76,10 @@ def parse_review_threads_response( field_name="response", invalid_value=type(response).__name__, expected_format="valid GraphQL response dictionary", - message=f"Response parsing failed due to type error: {str(e)}", + message=f"Response parsing failed due to type error: {e!s}", ) from e - def _parse_single_review_thread(self, thread_data: Dict[str, Any]) -> ReviewThread: + def _parse_single_review_thread(self, thread_data: dict[str, Any]) -> ReviewThread: """Parse a single review thread from GraphQL response data. Args: @@ -127,7 +126,7 @@ def _parse_single_review_thread(self, thread_data: Dict[str, Any]) -> ReviewThre expected_format="valid comment object", message=( f"Failed to parse comment at index {i} in thread " - f"{thread_id}: {str(e)}" + f"{thread_id}: {e!s}" ), ) from e @@ -146,7 +145,7 @@ def _parse_single_review_thread(self, thread_data: Dict[str, Any]) -> ReviewThre expected_format="list with valid comment objects", message=( f"Cannot extract thread metadata from comments in thread " - f"{thread_id}: {str(e)}" + f"{thread_id}: {e!s}" ), ) from e @@ -191,11 +190,11 @@ def _parse_single_review_thread(self, thread_data: Dict[str, Any]) -> ReviewThre field_name="thread_data", invalid_value=type(thread_data).__name__, expected_format="valid thread data dictionary", - message=f"Thread parsing failed due to type error: {str(e)}", + message=f"Thread parsing failed due to type error: {e!s}", ) from e def _parse_single_comment( - self, comment_data: Dict[str, Any], thread_id: str + self, comment_data: dict[str, Any], thread_id: str ) -> Comment: """Parse a single comment from GraphQL response data. @@ -230,7 +229,7 @@ def _parse_single_comment( field_name="comment.createdAt", invalid_value=comment_data.get("createdAt", "missing"), expected_format="valid ISO datetime string", - message=f"Failed to parse comment creation date: {str(e)}", + message=f"Failed to parse comment creation date: {e!s}", ) from e try: @@ -240,7 +239,7 @@ def _parse_single_comment( field_name="comment.updatedAt", invalid_value=comment_data.get("updatedAt", "missing"), expected_format="valid ISO datetime string", - message=f"Failed to parse comment update date: {str(e)}", + message=f"Failed to parse comment update date: {e!s}", ) from e # Extract author information with fallback @@ -306,7 +305,7 @@ def _parse_single_comment( field_name="comment_data", invalid_value=type(comment_data).__name__, expected_format="valid comment data dictionary", - message=f"Comment parsing failed due to type error: {str(e)}", + message=f"Comment parsing failed due to type error: {e!s}", ) from e def _extract_title_from_comment(self, content: str) -> str: @@ -328,8 +327,8 @@ def _extract_title_from_comment(self, content: str) -> str: return first_line or "Empty comment" def parse_paginated_response( - self, response: Dict[str, Any] - ) -> Tuple[List[ReviewThread], Optional[str]]: + self, response: dict[str, Any] + ) -> tuple[list[ReviewThread], Optional[str]]: """Parse a paginated GraphQL response for review threads. Args: @@ -380,12 +379,12 @@ def parse_paginated_response( field_name="response", invalid_value=type(response).__name__, expected_format="valid paginated GraphQL response", - message=f"Pagination parsing failed due to type error: {str(e)}", + message=f"Pagination parsing failed due to type error: {e!s}", ) from e def parse_pull_requests_response( - self, response: Dict[str, Any] - ) -> List[PullRequest]: + self, response: dict[str, Any] + ) -> list[PullRequest]: """Parse a GraphQL response containing pull requests. Args: @@ -451,10 +450,10 @@ def parse_pull_requests_response( field_name="response", invalid_value=type(response).__name__, expected_format="valid GraphQL pull requests response", - message=f"Response parsing failed due to type error: {str(e)}", + message=f"Response parsing failed due to type error: {e!s}", ) from e - def _parse_pull_request_data(self, pr_data: Dict[str, Any]) -> PullRequest: + def _parse_pull_request_data(self, pr_data: dict[str, Any]) -> PullRequest: """Parse individual pull request data into a PullRequest object. Args: @@ -485,7 +484,7 @@ def _parse_pull_request_data(self, pr_data: Dict[str, Any]) -> PullRequest: field_name="createdAt", invalid_value=pr_data.get("createdAt", "missing"), expected_format="ISO datetime string", - message=f"Invalid createdAt format: {str(e)}", + message=f"Invalid createdAt format: {e!s}", ) from e try: @@ -495,7 +494,7 @@ def _parse_pull_request_data(self, pr_data: Dict[str, Any]) -> PullRequest: field_name="updatedAt", invalid_value=pr_data.get("updatedAt", "missing"), expected_format="ISO datetime string", - message=f"Invalid updatedAt format: {str(e)}", + message=f"Invalid updatedAt format: {e!s}", ) from e # Create PullRequest object @@ -528,7 +527,7 @@ def _parse_pull_request_data(self, pr_data: Dict[str, Any]) -> PullRequest: field_name="pr_data", invalid_value=type(pr_data).__name__, expected_format="valid pull request data dictionary", - message=f"Pull request parsing failed due to type error: {str(e)}", + message=f"Pull request parsing failed due to type error: {e!s}", ) from e @@ -536,7 +535,7 @@ class ResponseValidator: """Validator for GitHub API response structures.""" @staticmethod - def validate_graphql_response(response: Dict[str, Any]) -> bool: + def validate_graphql_response(response: dict[str, Any]) -> bool: """Validate that a GraphQL response has the expected structure. Args: @@ -569,13 +568,12 @@ def validate_graphql_response(response: Dict[str, Any]) -> bool: message=f"GraphQL API errors: {'; '.join(error_messages)}", api_endpoint="GraphQL", ) - else: - raise create_validation_error( - field_name="data", - invalid_value="missing", - expected_format="data field in GraphQL response", - message="Response missing 'data' field", - ) + raise create_validation_error( + field_name="data", + invalid_value="missing", + expected_format="data field in GraphQL response", + message="Response missing 'data' field", + ) raise create_validation_error( field_name="data", invalid_value="missing", @@ -638,7 +636,7 @@ def validate_graphql_response(response: Dict[str, Any]) -> bool: return True @staticmethod - def validate_graphql_prs_response(response: Dict[str, Any]) -> bool: + def validate_graphql_prs_response(response: dict[str, Any]) -> bool: """Validate GraphQL response for pull requests has expected structure. Args: @@ -671,13 +669,12 @@ def validate_graphql_prs_response(response: Dict[str, Any]) -> bool: message=f"GraphQL API errors: {'; '.join(error_messages)}", api_endpoint="GraphQL", ) - else: - raise create_validation_error( - field_name="data", - invalid_value="missing", - expected_format="data field in GraphQL response", - message="Response missing 'data' field", - ) + raise create_validation_error( + field_name="data", + invalid_value="missing", + expected_format="data field in GraphQL response", + message="Response missing 'data' field", + ) raise create_validation_error( field_name="data", invalid_value="missing", @@ -723,7 +720,7 @@ def validate_graphql_prs_response(response: Dict[str, Any]) -> bool: return True @staticmethod - def validate_pull_request_data(pr_data: Dict[str, Any]) -> bool: + def validate_pull_request_data(pr_data: dict[str, Any]) -> bool: """Validate that pull request data has required fields. Args: @@ -764,7 +761,7 @@ def validate_pull_request_data(pr_data: Dict[str, Any]) -> bool: return True @staticmethod - def validate_review_thread_data(thread_data: Dict[str, Any]) -> bool: + def validate_review_thread_data(thread_data: dict[str, Any]) -> bool: """Validate that review thread data has required fields. Args: @@ -815,7 +812,7 @@ def validate_review_thread_data(thread_data: Dict[str, Any]) -> bool: return True @staticmethod - def validate_comment_data(comment_data: Dict[str, Any]) -> bool: + def validate_comment_data(comment_data: dict[str, Any]) -> bool: """Validate that comment data has required fields. Args: @@ -848,7 +845,7 @@ def validate_comment_data(comment_data: Dict[str, Any]) -> bool: return True @staticmethod - def validate_pull_requests_response(response: Dict[str, Any]) -> bool: + def validate_pull_requests_response(response: dict[str, Any]) -> bool: """Validate a GraphQL response has expected structure for pull requests. Args: diff --git a/src/toady/services/fetch_service.py b/src/toady/services/fetch_service.py index 00676d3..85a911d 100644 --- a/src/toady/services/fetch_service.py +++ b/src/toady/services/fetch_service.py @@ -1,6 +1,6 @@ """Fetch service for retrieving review threads from GitHub pull requests.""" -from typing import List, Optional, Tuple +from typing import Optional from ..models.models import PullRequest, ReviewThread from ..parsers.graphql_queries import ( @@ -15,21 +15,32 @@ class FetchServiceError(Exception): """Base exception for fetch service errors.""" - pass - class FetchService: """Service for fetching review threads from GitHub pull requests.""" - def __init__(self, github_service: Optional[GitHubService] = None) -> None: + def __init__( + self, + github_service: Optional[GitHubService] = None, + output_format: str = "pretty", + ) -> None: """Initialize the fetch service. Args: github_service: Optional GitHubService instance. If None, creates a new one. + output_format: Output format for PR selection messages ("json" or "pretty"). """ + allowed = {"json", "pretty"} + output_format = output_format.lower() + if output_format not in allowed: + raise ValueError( + f"Unsupported output_format '{output_format}'. " + f"Allowed: {', '.join(sorted(allowed))}" + ) + self.github_service = github_service or GitHubService() self.parser = GraphQLResponseParser() - self.pr_selector = PRSelector() + self.pr_selector = PRSelector(output_format=output_format) def fetch_review_threads( self, @@ -38,7 +49,7 @@ def fetch_review_threads( pr_number: int, include_resolved: bool = False, limit: int = 100, - ) -> List[ReviewThread]: + ) -> list[ReviewThread]: """Fetch review threads from a GitHub pull request. Args: @@ -86,7 +97,7 @@ def fetch_review_threads( # Wrap other exceptions in FetchServiceError raise FetchServiceError(f"Failed to fetch review threads: {e}") from e - def _get_repository_info(self) -> Tuple[str, str]: + def _get_repository_info(self) -> tuple[str, str]: """Get the current repository owner and name. Returns: @@ -113,7 +124,7 @@ def fetch_review_threads_from_current_repo( pr_number: int, include_resolved: bool = False, limit: int = 100, - ) -> List[ReviewThread]: + ) -> list[ReviewThread]: """Fetch review threads from a PR in the current repository. Args: @@ -147,7 +158,7 @@ def fetch_open_pull_requests( repo: str, include_drafts: bool = False, limit: int = 100, - ) -> List[PullRequest]: + ) -> list[PullRequest]: """Fetch open pull requests from a GitHub repository. Args: @@ -196,7 +207,7 @@ def fetch_open_pull_requests_from_current_repo( self, include_drafts: bool = False, limit: int = 100, - ) -> List[PullRequest]: + ) -> list[PullRequest]: """Fetch open pull requests from the current repository. Args: @@ -260,18 +271,17 @@ def select_pr_interactively( self.pr_selector.display_no_prs_message() return PRSelectionResult(pr_number=None, cancelled=False) - elif len(pull_requests) == 1: + if len(pull_requests) == 1: # Single PR - auto-select it selected_pr = pull_requests[0] self.pr_selector.display_auto_selected_pr(selected_pr) return PRSelectionResult(pr_number=selected_pr.number, cancelled=False) - else: - # Multiple PRs - show interactive selection - selected_pr_number = self.pr_selector.select_pr(pull_requests) - if selected_pr_number is None: - return PRSelectionResult(pr_number=None, cancelled=True) - return PRSelectionResult(pr_number=selected_pr_number, cancelled=False) + # Multiple PRs - show interactive selection + selected_pr_number = self.pr_selector.select_pr(pull_requests) + if selected_pr_number is None: + return PRSelectionResult(pr_number=None, cancelled=True) + return PRSelectionResult(pr_number=selected_pr_number, cancelled=False) except Exception as e: # Re-raise GitHub service exceptions as-is @@ -287,7 +297,7 @@ def fetch_review_threads_with_pr_selection( include_drafts: bool = False, threads_limit: int = 100, prs_limit: int = 100, - ) -> Tuple[List[ReviewThread], Optional[int]]: + ) -> tuple[list[ReviewThread], Optional[int]]: """Fetch review threads with optional interactive PR selection. If pr_number is provided, fetches threads from that PR directly. diff --git a/src/toady/services/github_service.py b/src/toady/services/github_service.py index 382fdb1..804760b 100644 --- a/src/toady/services/github_service.py +++ b/src/toady/services/github_service.py @@ -2,7 +2,7 @@ import json import subprocess -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Optional # GraphQL mutation constants REPLY_THREAD_MUTATION = """ @@ -98,38 +98,26 @@ class GitHubServiceError(Exception): """Base exception for GitHub service errors.""" - pass - class GitHubCLINotFoundError(GitHubServiceError): """Raised when gh CLI is not found or not installed.""" - pass - class GitHubAuthenticationError(GitHubServiceError): """Raised when gh CLI authentication fails.""" - pass - class GitHubAPIError(GitHubServiceError): """Raised when GitHub API calls fail.""" - pass - class GitHubTimeoutError(GitHubServiceError): """Raised when GitHub CLI commands timeout.""" - pass - class GitHubRateLimitError(GitHubServiceError): """Raised when GitHub API rate limit is exceeded.""" - pass - class GitHubService: """Service for interacting with GitHub through the gh CLI.""" @@ -242,7 +230,7 @@ def validate_version_compatibility(self, min_version: str = "2.0.0") -> bool: return current_parts >= min_parts - def run_gh_command(self, args: List[str], timeout: Optional[int] = None) -> Any: + def run_gh_command(self, args: list[str], timeout: Optional[int] = None) -> Any: """Run a gh CLI command with error handling and timeout support. Args: @@ -312,7 +300,7 @@ def run_gh_command(self, args: List[str], timeout: Optional[int] = None) -> Any: "gh CLI is not installed or not accessible" ) from e - def get_json_output(self, args: List[str]) -> Any: + def get_json_output(self, args: list[str]) -> Any: """Run a gh CLI command and parse JSON output. Args: @@ -352,8 +340,8 @@ def get_current_repo(self) -> Optional[str]: return None def execute_graphql_query( - self, query: str, variables: Optional[Dict[str, Any]] = None - ) -> Dict[str, Any]: + self, query: str, variables: Optional[dict[str, Any]] = None + ) -> dict[str, Any]: """Execute a GraphQL query using gh CLI. Args: @@ -396,7 +384,7 @@ def execute_graphql_query( except json.JSONDecodeError as e: raise GitHubAPIError(f"Failed to parse GraphQL response: {e}") from e - def get_repo_info_from_url(self, repo_url: str) -> Tuple[str, str]: + def get_repo_info_from_url(self, repo_url: str) -> tuple[str, str]: """Extract owner and repository name from a GitHub URL. Args: @@ -482,7 +470,7 @@ def check_pr_exists(self, owner: str, repo: str, pr_number: int) -> bool: def post_reply( self, comment_id: str, body: str, review_id: Optional[str] = None - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Post a reply to a review comment or thread. Args: @@ -510,33 +498,32 @@ def post_reply( if strategy == "thread_reply": # Use thread reply mutation for node IDs - from .node_id_validation import create_thread_validator + from ..validators.node_id_validation import create_thread_validator validator = create_thread_validator() validator.validate_id(comment_id, "Thread ID") variables = {"threadId": comment_id, "body": body} return self.execute_graphql_query(REPLY_THREAD_MUTATION, variables) - else: - # Use comment reply mutation for numeric/node IDs needing review context - from .node_id_validation import validate_comment_id + # Use comment reply mutation for numeric/node IDs needing review context + from ..validators.node_id_validation import validate_comment_id - validate_comment_id(comment_id) + validate_comment_id(comment_id) - # If review_id is not provided, try to fetch it for node IDs - if not review_id and not comment_id.isdigit(): - review_id = self._get_review_id_for_comment(comment_id) + # If review_id is not provided, try to fetch it for node IDs + if not review_id and not comment_id.isdigit(): + review_id = self._get_review_id_for_comment(comment_id) - if not review_id: - raise ValueError( - "Review ID is required for comment replies. " - "Could not determine review ID from comment." - ) + if not review_id: + raise ValueError( + "Review ID is required for comment replies. " + "Could not determine review ID from comment." + ) - variables = {"reviewId": review_id, "commentId": comment_id, "body": body} - return self.execute_graphql_query(REPLY_COMMENT_MUTATION, variables) + variables = {"reviewId": review_id, "commentId": comment_id, "body": body} + return self.execute_graphql_query(REPLY_COMMENT_MUTATION, variables) - def resolve_thread(self, thread_id: str, undo: bool = False) -> Dict[str, Any]: + def resolve_thread(self, thread_id: str, undo: bool = False) -> dict[str, Any]: """Resolve or unresolve a review thread. Args: @@ -556,7 +543,7 @@ def resolve_thread(self, thread_id: str, undo: bool = False) -> Dict[str, Any]: thread_id = thread_id.strip() # Validate thread ID - from .node_id_validation import validate_thread_id + from ..validators.node_id_validation import validate_thread_id validate_thread_id(thread_id) @@ -582,11 +569,10 @@ def _determine_reply_strategy(self, comment_id: str) -> str: # Comment IDs start with IC_, PRRC_, RP_ if comment_id.startswith(("PRT_", "PRRT_", "RT_")): return "thread_reply" - elif comment_id.startswith(("IC_", "PRRC_", "RP_")): - return "comment_reply" - else: - # Default to comment reply for unknown formats + if comment_id.startswith(("IC_", "PRRC_", "RP_")): return "comment_reply" + # Default to comment reply for unknown formats + return "comment_reply" def _get_review_id_for_comment(self, comment_id: str) -> Optional[str]: """Get the review ID associated with a comment node ID. @@ -644,7 +630,7 @@ def _get_review_id_for_comment(self, comment_id: str) -> Optional[str]: def fetch_open_pull_requests( self, owner: str, repo: str, include_drafts: bool = False, limit: int = 100 - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Fetch open pull requests from a repository. Args: @@ -688,7 +674,7 @@ def fetch_open_pull_requests( raise GitHubAPIError("Repository not found or no access") pull_requests_data = repository_data.get("pullRequests", {}) - pr_nodes: List[Dict[str, Any]] = pull_requests_data.get("nodes", []) + pr_nodes: list[dict[str, Any]] = pull_requests_data.get("nodes", []) # Filter out drafts if not included if query_builder.should_filter_drafts(): diff --git a/src/toady/services/pr_selection.py b/src/toady/services/pr_selection.py index 1bc4b5c..0a2fc37 100644 --- a/src/toady/services/pr_selection.py +++ b/src/toady/services/pr_selection.py @@ -1,6 +1,6 @@ """Pull request selection logic for automatic detection and user selection.""" -from typing import List, NoReturn, Optional, Union +from typing import NoReturn, Optional, Union import click @@ -11,18 +11,15 @@ class PRSelectionError(Exception): """Exception raised when PR selection fails.""" - pass - class PRSelector: """Handles pull request selection logic for different scenarios.""" def __init__(self) -> None: """Initialize the PR selector.""" - pass def select_pull_request( - self, pull_requests: List[PullRequest], allow_multiple: bool = True + self, pull_requests: list[PullRequest], allow_multiple: bool = True ) -> Union[int, None]: """Select a pull request from the available options. @@ -57,12 +54,11 @@ def select_pull_request( # Scenario 3: Multiple open PRs if allow_multiple: return self._handle_multiple_prs(pull_requests) - else: - # If not allowing multiple selection, treat as error - raise PRSelectionError( - f"Found {len(pull_requests)} open pull requests, but multiple " - "selection is not allowed in this context" - ) + # If not allowing multiple selection, treat as error + raise PRSelectionError( + f"Found {len(pull_requests)} open pull requests, but multiple " + "selection is not allowed in this context" + ) def _handle_no_prs(self) -> NoReturn: """Handle the case when no open PRs are found. @@ -91,7 +87,7 @@ def _handle_single_pr(self, pull_request: PullRequest) -> int: ) return pull_request.number - def _handle_multiple_prs(self, pull_requests: List[PullRequest]) -> Optional[int]: + def _handle_multiple_prs(self, pull_requests: list[PullRequest]) -> Optional[int]: """Handle the case when multiple open PRs are found. Args: @@ -173,10 +169,10 @@ def _handle_multiple_prs(self, pull_requests: List[PullRequest]) -> Optional[int except Exception as e: if isinstance(e, PRSelectionError): raise - raise PRSelectionError(f"Selection failed: {str(e)}") from e + raise PRSelectionError(f"Selection failed: {e!s}") from e def validate_pr_exists( - self, pr_number: int, pull_requests: List[PullRequest] + self, pr_number: int, pull_requests: list[PullRequest] ) -> bool: """Validate that a specific PR number exists in the list. diff --git a/src/toady/services/pr_selector.py b/src/toady/services/pr_selector.py index 79044b8..4afa5b0 100644 --- a/src/toady/services/pr_selector.py +++ b/src/toady/services/pr_selector.py @@ -1,6 +1,6 @@ """Interactive pull request selection interface.""" -from typing import List, Optional +from typing import Optional import click @@ -15,11 +15,18 @@ class PRSelector: with formatted display and navigation controls. """ - def __init__(self) -> None: - """Initialize the PR selector.""" + def __init__(self, output_format: str = "pretty") -> None: + """Initialize the PR selector. + + Args: + output_format: Output format ("json" or "pretty"). Controls whether + interactive messages are displayed or suppressed. + """ self.formatter = PrettyFormatter() + self.output_format = output_format + self.is_json_mode = output_format == "json" - def select_pr(self, pull_requests: List[PullRequest]) -> Optional[int]: + def select_pr(self, pull_requests: list[PullRequest]) -> Optional[int]: """Select a PR from the given list through interactive interface. Args: @@ -38,11 +45,22 @@ def select_pr(self, pull_requests: List[PullRequest]) -> Optional[int]: # Auto-select single PR return pull_requests[0].number - # Multiple PRs - show interactive selection + # Multiple PRs + if self.is_json_mode: + # In JSON mode, we can't do interactive selection + # Send error message to stderr and return None + click.echo( + "Error: Multiple PRs found but interactive selection not available " + "in JSON mode. Please specify --pr option.", + err=True, + ) + return None + + # Show interactive selection (pretty mode only) return self._show_pr_selection_menu(pull_requests) def _show_pr_selection_menu( - self, pull_requests: List[PullRequest] + self, pull_requests: list[PullRequest] ) -> Optional[int]: """Display interactive PR selection menu. @@ -53,11 +71,12 @@ def _show_pr_selection_menu( Selected PR number, or None if cancelled """ # Display header - click.echo() + # In JSON mode, send to stderr to avoid polluting JSON output + click.echo(err=self.is_json_mode) plural = "s" if len(pull_requests) != 1 else "" header = f"๐Ÿ“‹ Found {len(pull_requests)} open pull request{plural}" - click.echo(click.style(header, bold=True, fg="cyan")) - click.echo() + click.echo(click.style(header, bold=True, fg="cyan"), err=self.is_json_mode) + click.echo(err=self.is_json_mode) # Display PR list with formatting self._display_pr_list(pull_requests) @@ -65,7 +84,7 @@ def _show_pr_selection_menu( # Get user selection return self._prompt_for_selection(pull_requests) - def _display_pr_list(self, pull_requests: List[PullRequest]) -> None: + def _display_pr_list(self, pull_requests: list[PullRequest]) -> None: """Display formatted list of pull requests. Args: @@ -98,11 +117,18 @@ def _display_pr_list(self, pull_requests: List[PullRequest]) -> None: thread_info = thread_count # Combine all components - click.echo(f" {number_style} {pr_number} {title}{draft_indicator}") - click.echo(f" {author_info} โ€ข {branch_info}{thread_info}") - click.echo() - - def _prompt_for_selection(self, pull_requests: List[PullRequest]) -> Optional[int]: + # In JSON mode, send to stderr to avoid polluting JSON output + click.echo( + f" {number_style} {pr_number} {title}{draft_indicator}", + err=self.is_json_mode, + ) + click.echo( + f" {author_info} โ€ข {branch_info}{thread_info}", + err=self.is_json_mode, + ) + click.echo(err=self.is_json_mode) + + def _prompt_for_selection(self, pull_requests: list[PullRequest]) -> Optional[int]: """Prompt user for PR selection with validation. Args: @@ -177,6 +203,11 @@ def _prompt_for_selection(self, pull_requests: List[PullRequest]) -> Optional[in def display_no_prs_message(self) -> None: """Display message when no open PRs are found.""" + # In JSON mode, completely suppress output to avoid compatibility issues + if self.is_json_mode: + return + + # Pretty mode - show the message click.echo() message = "๐Ÿ“ No open pull requests found in this repository." click.echo(click.style(message, fg="yellow", bold=True)) @@ -193,6 +224,11 @@ def display_auto_selected_pr(self, pr: PullRequest) -> None: Args: pr: The automatically selected pull request """ + # In JSON mode, completely suppress output to avoid compatibility issues + if self.is_json_mode: + return + + # Pretty mode - show the selection message click.echo() header = "๐ŸŽฏ Auto-selected the only open pull request:" click.echo(click.style(header, fg="green", bold=True)) diff --git a/src/toady/services/reply_service.py b/src/toady/services/reply_service.py index 9653172..34205b2 100644 --- a/src/toady/services/reply_service.py +++ b/src/toady/services/reply_service.py @@ -1,8 +1,8 @@ """Reply service for posting comments to GitHub pull request reviews.""" -import json from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Tuple +import json +from typing import Any, Optional from .github_service import GitHubAPIError, GitHubService, GitHubServiceError @@ -10,14 +10,10 @@ class ReplyServiceError(GitHubServiceError): """Base exception for reply service errors.""" - pass - class CommentNotFoundError(ReplyServiceError): """Raised when the specified comment cannot be found.""" - pass - @dataclass class ReplyRequest: @@ -42,7 +38,7 @@ def __init__(self, github_service: Optional[GitHubService] = None) -> None: def post_reply( self, request: ReplyRequest, fetch_context: bool = False - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Post a reply to a pull request review comment using GraphQL mutations. This method now uses GraphQL mutations instead of REST API calls for better @@ -139,7 +135,7 @@ def post_reply( raise ReplyServiceError(f"Failed to post reply: {e}") from e def _handle_graphql_errors( - self, errors: List[Dict[str, Any]], comment_id: str + self, errors: list[dict[str, Any]], comment_id: str ) -> None: """Handle GraphQL errors and raise appropriate exceptions. @@ -167,12 +163,12 @@ def _handle_graphql_errors( def _build_reply_info_from_graphql( self, - comment_data: Dict[str, Any], + comment_data: dict[str, Any], request: ReplyRequest, fetch_context: bool, owner: str, repo: str, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Build reply information dictionary from GraphQL response data. Args: @@ -211,7 +207,7 @@ def _build_reply_info_from_graphql( def _post_reply_fallback_rest( self, request: ReplyRequest, fetch_context: bool, owner: str, repo: str - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Fallback to REST API for posting replies. This method uses the original REST API approach for backward compatibility. @@ -277,7 +273,7 @@ def _post_reply_fallback_rest( f"Comment {request.comment_id} not found" ) from e # Check if it's a "review already submitted" error - elif "review has already been submitted" in str(e).lower(): + if "review has already been submitted" in str(e).lower(): raise ReplyServiceError( f"Cannot reply to comment {request.comment_id} because the review " f"has already been submitted. Try using the thread ID instead, or " @@ -345,7 +341,7 @@ def _get_review_id_for_comment( raise CommentNotFoundError(f"Comment {comment_id} not found") from e raise ReplyServiceError(f"Failed to get review ID: {e}") from e - def _get_repository_info(self) -> Tuple[str, str]: + def _get_repository_info(self) -> tuple[str, str]: """Get the current repository owner and name. Returns: @@ -398,7 +394,7 @@ def validate_comment_exists( def _get_parent_comment_info( self, owner: str, repo: str, comment_id: str - ) -> Optional[Dict[str, str]]: + ) -> Optional[dict[str, str]]: """Get information about the parent comment for additional context. Args: @@ -455,7 +451,7 @@ def _get_parent_comment_info( def _get_pr_info( self, owner: str, repo: str, pr_number: str - ) -> Optional[Dict[str, str]]: + ) -> Optional[dict[str, str]]: """Get basic PR information. Args: diff --git a/src/toady/services/resolve_service.py b/src/toady/services/resolve_service.py index 9efe20e..a11db55 100644 --- a/src/toady/services/resolve_service.py +++ b/src/toady/services/resolve_service.py @@ -1,7 +1,7 @@ """Service for resolving and unresolving review threads via GitHub GraphQL API.""" import logging -from typing import Any, Dict, List, Optional +from typing import Any, Optional from ..exceptions import ( GitHubAPIError, @@ -31,7 +31,7 @@ def __init__(self, github_service: Optional[GitHubService] = None) -> None: """ self.github_service = github_service or GitHubService() - def resolve_thread(self, thread_id: str) -> Dict[str, Any]: + def resolve_thread(self, thread_id: str) -> dict[str, Any]: """Resolve a review thread. Args: @@ -56,7 +56,7 @@ def resolve_thread(self, thread_id: str) -> Dict[str, Any]: field_name="thread_id", invalid_value=thread_id, expected_format="valid GitHub thread ID", - message=f"Invalid thread ID format: {str(e)}", + message=f"Invalid thread ID format: {e!s}", ) from e # Execute GraphQL mutation with error handling @@ -69,7 +69,7 @@ def resolve_thread(self, thread_id: str) -> Dict[str, Any]: raise except Exception as e: raise create_github_error( - message=f"Failed to execute resolve mutation: {str(e)}", + message=f"Failed to execute resolve mutation: {e!s}", api_endpoint="GraphQL resolve mutation", ) from e @@ -104,7 +104,7 @@ def resolve_thread(self, thread_id: str) -> Dict[str, Any]: except (KeyError, TypeError, AttributeError) as e: raise ResolveServiceError( message=( - "Invalid response structure from resolve mutation: " f"{str(e)}" + "Invalid response structure from resolve mutation: " f"{e!s}" ), context={ "thread_id": thread_id, @@ -129,11 +129,11 @@ def resolve_thread(self, thread_id: str) -> Dict[str, Any]: except Exception as e: # Wrap any unexpected errors raise ResolveServiceError( - message=f"Unexpected error during thread resolution: {str(e)}", + message=f"Unexpected error during thread resolution: {e!s}", context={"thread_id": thread_id, "action": "resolve"}, ) from e - def unresolve_thread(self, thread_id: str) -> Dict[str, Any]: + def unresolve_thread(self, thread_id: str) -> dict[str, Any]: """Unresolve a review thread. Args: @@ -158,7 +158,7 @@ def unresolve_thread(self, thread_id: str) -> Dict[str, Any]: field_name="thread_id", invalid_value=thread_id, expected_format="valid GitHub thread ID", - message=f"Invalid thread ID format: {str(e)}", + message=f"Invalid thread ID format: {e!s}", ) from e # Execute GraphQL mutation with error handling @@ -171,7 +171,7 @@ def unresolve_thread(self, thread_id: str) -> Dict[str, Any]: raise except Exception as e: raise create_github_error( - message=f"Failed to execute unresolve mutation: {str(e)}", + message=f"Failed to execute unresolve mutation: {e!s}", api_endpoint="GraphQL unresolve mutation", ) from e @@ -206,8 +206,7 @@ def unresolve_thread(self, thread_id: str) -> Dict[str, Any]: except (KeyError, TypeError, AttributeError) as e: raise ResolveServiceError( message=( - "Invalid response structure from unresolve mutation: " - f"{str(e)}" + "Invalid response structure from unresolve mutation: " f"{e!s}" ), context={ "thread_id": thread_id, @@ -232,12 +231,12 @@ def unresolve_thread(self, thread_id: str) -> Dict[str, Any]: except Exception as e: # Wrap any unexpected errors raise ResolveServiceError( - message=f"Unexpected error during thread unresolution: {str(e)}", + message=f"Unexpected error during thread unresolution: {e!s}", context={"thread_id": thread_id, "action": "unresolve"}, ) from e def _handle_graphql_errors( - self, errors: List[Dict[str, Any]], thread_id: str, action: str + self, errors: list[dict[str, Any]], thread_id: str, action: str ) -> None: """Handle GraphQL errors and raise appropriate exceptions. @@ -267,7 +266,7 @@ def _handle_graphql_errors( if not isinstance(error, dict): # mypy: disable-error-code=unreachable error_messages.append( - f"Invalid error format at index {i}: {str(error)}" + f"Invalid error format at index {i}: {error!s}" ) continue @@ -284,7 +283,7 @@ def _handle_graphql_errors( message=f"Thread {thread_id} not found", thread_id=thread_id, ) - elif ( + if ( "permission" in message_lower or "forbidden" in message_lower or "not accessible" in message_lower @@ -297,17 +296,14 @@ def _handle_graphql_errors( ), thread_id=thread_id, ) - else: - # If we get here, it's a generic error message - error_messages.append(message) + # If we get here, it's a generic error message + error_messages.append(message) except (ThreadNotFoundError, ThreadPermissionError): # Re-raise these immediately raise except Exception as e: # Continue processing other errors - error_messages.append( - f"Error processing GraphQL error {i}: {str(e)}" - ) + error_messages.append(f"Error processing GraphQL error {i}: {e!s}") # If we get here, it's a generic GraphQL error combined_message = ( @@ -328,7 +324,7 @@ def _handle_graphql_errors( except Exception as e: # Wrap any unexpected errors in error handling raise ResolveServiceError( - message=f"Error processing GraphQL errors during {action}: {str(e)}", + message=f"Error processing GraphQL errors during {action}: {e!s}", context={"thread_id": thread_id, "action": action}, ) from e @@ -425,7 +421,7 @@ def validate_thread_exists( raise except Exception as e: raise create_github_error( - message=f"Failed to execute thread validation query: {str(e)}", + message=f"Failed to execute thread validation query: {e!s}", api_endpoint="GraphQL node validation", ) from e @@ -517,7 +513,7 @@ def validate_thread_exists( raise ResolveServiceError( message=( "Failed to validate thread existence due to unexpected " - f"response structure: {str(e)}" + f"response structure: {e!s}" ), context={ "thread_id": thread_id, @@ -543,7 +539,7 @@ def validate_thread_exists( raise ResolveServiceError( message=( "Failed to validate thread existence due to unexpected " - f"error: {str(e)}" + f"error: {e!s}" ), context={ "thread_id": thread_id, @@ -553,7 +549,7 @@ def validate_thread_exists( }, ) from e - def _get_thread_url(self, thread_data: Dict[str, Any], thread_id: str) -> str: + def _get_thread_url(self, thread_data: dict[str, Any], thread_id: str) -> str: """Extract thread URL from GraphQL response with intelligent fallback. Args: diff --git a/src/toady/utils.py b/src/toady/utils.py index 93ec6a4..07fbe7f 100644 --- a/src/toady/utils.py +++ b/src/toady/utils.py @@ -1,7 +1,7 @@ """Utility functions for the toady package.""" -import json from datetime import datetime +import json import click @@ -59,7 +59,7 @@ def parse_datetime(date_str: str) -> datetime: field_name="date_str", invalid_value=original_date_str, expected_format="valid ISO datetime string", - message=f"Failed to process timezone in datetime string: {str(e)}", + message=f"Failed to process timezone in datetime string: {e!s}", ) from e # Try parsing with different formats @@ -73,7 +73,7 @@ def parse_datetime(date_str: str) -> datetime: try: return datetime.strptime(date_str, fmt) except ValueError as e: - parsing_errors.append(f"Format '{fmt}': {str(e)}") + parsing_errors.append(f"Format '{fmt}': {e!s}") continue # If we get here, all formats failed @@ -98,7 +98,7 @@ def parse_datetime(date_str: str) -> datetime: field_name="date_str", invalid_value=str(date_str) if "date_str" in locals() else "unknown", expected_format="valid ISO datetime string", - message=f"Unexpected error parsing datetime: {str(e)}", + message=f"Unexpected error parsing datetime: {e!s}", ) from e diff --git a/src/toady/validators/node_id_validation.py b/src/toady/validators/node_id_validation.py index e68ebb2..65aaddb 100644 --- a/src/toady/validators/node_id_validation.py +++ b/src/toady/validators/node_id_validation.py @@ -5,9 +5,9 @@ the entity type. """ -import re from enum import Enum -from typing import List, Optional, Set +import re +from typing import Optional class GitHubEntityType(Enum): @@ -66,7 +66,7 @@ class NodeIDValidator: REVIEW_TYPES = {GitHubEntityType.PULL_REQUEST_REVIEW} - def __init__(self, allowed_types: Optional[Set[GitHubEntityType]] = None): + def __init__(self, allowed_types: Optional[set[GitHubEntityType]] = None): """Initialize validator with optional allowed entity types. Args: @@ -77,7 +77,7 @@ def __init__(self, allowed_types: Optional[Set[GitHubEntityType]] = None): ) self._prefix_to_type = {entity.value: entity for entity in GitHubEntityType} - def get_allowed_prefixes(self) -> List[str]: + def get_allowed_prefixes(self) -> list[str]: """Get list of allowed node ID prefixes based on allowed types. Returns: diff --git a/src/toady/validators/schema_validator.py b/src/toady/validators/schema_validator.py index 142d600..1bd51d8 100644 --- a/src/toady/validators/schema_validator.py +++ b/src/toady/validators/schema_validator.py @@ -5,13 +5,13 @@ breaking changes early. """ +from datetime import datetime, timedelta import hashlib import json import logging -import subprocess -from datetime import datetime, timedelta from pathlib import Path -from typing import Any, Dict, List, Optional +import subprocess +from typing import Any, Optional from ..parsers.graphql_parser import GraphQLField, GraphQLParser from ..services.github_service import GitHubService @@ -25,8 +25,8 @@ class SchemaValidationError(Exception): def __init__( self, message: str, - errors: Optional[List[Dict[str, Any]]] = None, - suggestions: Optional[List[str]] = None, + errors: Optional[list[dict[str, Any]]] = None, + suggestions: Optional[list[str]] = None, ): """Initialize schema validation error. @@ -75,8 +75,8 @@ def __init__( """ self.cache_dir = cache_dir or Path.home() / ".toady" / "cache" self.cache_ttl = cache_ttl - self._schema: Optional[Dict[str, Any]] = None - self._type_map: Optional[Dict[str, Any]] = None + self._schema: Optional[dict[str, Any]] = None + self._type_map: Optional[dict[str, Any]] = None self._github_service = GitHubService() def _get_cache_path(self) -> Path: @@ -103,7 +103,7 @@ def _is_cache_valid(self) -> bool: except (json.JSONDecodeError, KeyError, ValueError): return False - def _load_cached_schema(self) -> Optional[Dict[str, Any]]: + def _load_cached_schema(self) -> Optional[dict[str, Any]]: """Load schema from cache if valid.""" if not self._is_cache_valid(): return None @@ -120,7 +120,7 @@ def _load_cached_schema(self) -> Optional[Dict[str, Any]]: logger.warning("Failed to load cached schema") return None - def _save_schema_to_cache(self, schema: Dict[str, Any]) -> None: + def _save_schema_to_cache(self, schema: dict[str, Any]) -> None: """Save schema to cache with metadata.""" cache_path = self._get_cache_path() metadata_path = self._get_cache_metadata_path() @@ -139,7 +139,7 @@ def _save_schema_to_cache(self, schema: Dict[str, Any]) -> None: with open(metadata_path, "w") as f: json.dump(metadata, f, indent=2) - def fetch_schema(self, force_refresh: bool = False) -> Dict[str, Any]: + def fetch_schema(self, force_refresh: bool = False) -> dict[str, Any]: """Fetch the GitHub GraphQL schema. Args: @@ -182,8 +182,7 @@ def fetch_schema(self, force_refresh: bool = False) -> Dict[str, Any]: logger.info("Successfully fetched GitHub schema") return schema - else: - raise SchemaValidationError("Schema data is not a dictionary") + raise SchemaValidationError("Schema data is not a dictionary") except subprocess.CalledProcessError as e: raise SchemaValidationError(f"Failed to fetch GitHub schema: {e}") from e @@ -202,7 +201,7 @@ def _build_type_map(self) -> None: if type_def.get("name"): self._type_map[type_def["name"]] = type_def - def get_type(self, type_name: str) -> Optional[Dict[str, Any]]: + def get_type(self, type_name: str) -> Optional[dict[str, Any]]: """Get a type definition by name. Args: @@ -216,7 +215,7 @@ def get_type(self, type_name: str) -> Optional[Dict[str, Any]]: return self._type_map.get(type_name) if self._type_map else None - def validate_query(self, query: str) -> List[Dict[str, Any]]: + def validate_query(self, query: str) -> list[dict[str, Any]]: """Validate a GraphQL query against the schema. Args: @@ -248,7 +247,7 @@ def validate_query(self, query: str) -> List[Dict[str, Any]]: errors.append( { "type": "parse_error", - "message": f"Failed to parse query: {str(e)}", + "message": f"Failed to parse query: {e!s}", } ) return errors @@ -274,10 +273,10 @@ def validate_query(self, query: str) -> List[Dict[str, Any]]: def _validate_selections( self, - selections: List[GraphQLField], - parent_type: Dict[str, Any], - errors: List[Dict[str, Any]], - type_path: List[str], + selections: list[GraphQLField], + parent_type: dict[str, Any], + errors: list[dict[str, Any]], + type_path: list[str], ) -> None: """Validate field selections against a type. @@ -366,9 +365,9 @@ def _validate_selections( def _validate_arguments( self, field: GraphQLField, - field_def: Dict[str, Any], - errors: List[Dict[str, Any]], - type_path: List[str], + field_def: dict[str, Any], + errors: list[dict[str, Any]], + type_path: list[str], ) -> None: """Validate field arguments against schema. @@ -409,7 +408,7 @@ def _validate_arguments( } ) - def _resolve_field_type(self, type_ref: Optional[Dict[str, Any]]) -> Optional[str]: + def _resolve_field_type(self, type_ref: Optional[dict[str, Any]]) -> Optional[str]: """Resolve the actual type name from a type reference. Args: @@ -427,7 +426,7 @@ def _resolve_field_type(self, type_ref: Optional[Dict[str, Any]]) -> Optional[st return type_ref.get("name") if type_ref else None - def _is_required_type(self, type_ref: Optional[Dict[str, Any]]) -> bool: + def _is_required_type(self, type_ref: Optional[dict[str, Any]]) -> bool: """Check if a type is required (NON_NULL). Args: @@ -438,7 +437,7 @@ def _is_required_type(self, type_ref: Optional[Dict[str, Any]]) -> bool: """ return bool(type_ref and type_ref.get("kind") == "NON_NULL") - def check_deprecations(self, query: str) -> List[Dict[str, Any]]: + def check_deprecations(self, query: str) -> list[dict[str, Any]]: """Check for deprecated fields in a query. Args: @@ -450,7 +449,7 @@ def check_deprecations(self, query: str) -> List[Dict[str, Any]]: if not self._schema: self.fetch_schema() - warnings: List[Dict[str, Any]] = [] + warnings: list[dict[str, Any]] = [] # TODO: Implement deprecation checking by parsing query # and checking each field against schema @@ -473,7 +472,7 @@ def get_schema_version(self) -> Optional[str]: schema_str = json.dumps(self._schema, sort_keys=True) return hashlib.sha256(schema_str.encode()).hexdigest()[:12] - def get_field_suggestions(self, type_name: str, field_name: str) -> List[str]: + def get_field_suggestions(self, type_name: str, field_name: str) -> list[str]: """Get suggestions for a field name on a type. Args: @@ -498,7 +497,7 @@ def get_field_suggestions(self, type_name: str, field_name: str) -> List[str]: return suggestions[:5] # Limit to top 5 suggestions - def validate_mutations(self) -> Dict[str, List[Dict[str, Any]]]: + def validate_mutations(self) -> dict[str, list[dict[str, Any]]]: """Validate all mutations defined in the codebase. Returns: @@ -534,7 +533,7 @@ def validate_mutations(self) -> Dict[str, List[Dict[str, Any]]]: return errors - def validate_queries(self) -> Dict[str, List[Dict[str, Any]]]: + def validate_queries(self) -> dict[str, list[dict[str, Any]]]: """Validate all queries defined in the codebase. Returns: @@ -555,13 +554,13 @@ def validate_queries(self) -> Dict[str, List[Dict[str, Any]]]: return errors - def generate_compatibility_report(self) -> Dict[str, Any]: + def generate_compatibility_report(self) -> dict[str, Any]: """Generate a comprehensive compatibility report. Returns: Report containing validation results and recommendations """ - report: Dict[str, Any] = { + report: dict[str, Any] = { "timestamp": datetime.now().isoformat(), "schema_version": self.get_schema_version(), "queries": self.validate_queries(), @@ -571,7 +570,7 @@ def generate_compatibility_report(self) -> Dict[str, Any]: } # Add recommendations based on errors - all_errors: List[Dict[str, Any]] = [] + all_errors: list[dict[str, Any]] = [] queries = report.get("queries", {}) mutations = report.get("mutations", {}) if isinstance(queries, dict): diff --git a/src/toady/validators/validation.py b/src/toady/validators/validation.py index f6ef9c3..f831a2f 100644 --- a/src/toady/validators/validation.py +++ b/src/toady/validators/validation.py @@ -4,10 +4,10 @@ configuration parameters, and data types used throughout the application. """ -import re from dataclasses import dataclass from datetime import datetime -from typing import Any, Dict, List, Optional, Union +import re +from typing import Any, Optional, Union from ..exceptions import ValidationError, create_validation_error from ..utils import MAX_PR_NUMBER @@ -477,13 +477,12 @@ def validate_datetime_string(date_str: str, field_name: str = "Date") -> datetim expected_format="ISO datetime string (e.g., '2024-01-01T12:00:00')", message=f"Invalid {field_name.lower()} format", ) from e - else: - raise create_validation_error( - field_name=field_name, - invalid_value=date_str, - expected_format="ISO datetime string (e.g., '2024-01-01T12:00:00')", - message=f"Invalid {field_name.lower()} format: {str(e)}", - ) from e + raise create_validation_error( + field_name=field_name, + invalid_value=date_str, + expected_format="ISO datetime string (e.g., '2024-01-01T12:00:00')", + message=f"Invalid {field_name.lower()} format: {e!s}", + ) from e def validate_email(email: str, field_name: str = "Email") -> str: @@ -745,7 +744,7 @@ def validate_boolean_flag( def validate_choice( value: Any, - choices: List[Any], + choices: list[Any], field_name: str = "Value", case_sensitive: bool = True, ) -> Any: @@ -791,11 +790,11 @@ def validate_choice( def validate_dict_keys( - data: Dict[str, Any], - required_keys: List[str], - optional_keys: Optional[List[str]] = None, + data: dict[str, Any], + required_keys: list[str], + optional_keys: Optional[list[str]] = None, field_name: str = "Data", -) -> Dict[str, Any]: +) -> dict[str, Any]: """Validate that a dictionary contains required keys and no unexpected keys. Args: @@ -863,7 +862,7 @@ def validate_dict_keys( # Convenience function for validating reply content warnings -def check_reply_content_warnings(body: str) -> List[str]: +def validate_reply_content_warnings(body: str) -> list[str]: """Check for potential issues in reply content and return warnings. Args: @@ -899,7 +898,7 @@ def validate_fetch_command_args( pretty: bool = False, resolved: bool = False, limit: Union[int, str] = 100, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Validate all arguments for the fetch command. Args: @@ -927,7 +926,7 @@ def validate_reply_command_args( body: str, pretty: bool = False, verbose: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Validate all arguments for the reply command. Args: @@ -969,7 +968,7 @@ def validate_resolve_command_args( thread_id: Optional[Union[str, int]] = None, pr_number: Optional[Union[int, str]] = None, options: Optional[ResolveOptions] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Validate all arguments for the resolve command. Args: diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..88dafc0 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,185 @@ +# Test Framework Documentation + +This document describes the comprehensive test framework setup for the Toady CLI project. + +## Test Organization + +The test suite is organized into the following structure: + +``` +tests/ +โ”œโ”€โ”€ conftest.py # Shared fixtures and configuration +โ”œโ”€โ”€ unit/ # Unit tests (isolated, fast) +โ”‚ โ”œโ”€โ”€ formatters/ # Output formatting tests +โ”‚ โ”œโ”€โ”€ models/ # Data model tests +โ”‚ โ”œโ”€โ”€ parsers/ # Data parsing tests +โ”‚ โ”œโ”€โ”€ services/ # Business logic tests +โ”‚ โ””โ”€โ”€ validators/ # Input validation tests +โ”œโ”€โ”€ integration/ # Integration tests +โ”‚ โ”œโ”€โ”€ cli/ # CLI command integration tests +โ”‚ โ””โ”€โ”€ test_*.py # End-to-end integration tests +โ””โ”€โ”€ test_*.py # General tests and examples +``` + +## Test Markers + +The framework uses pytest markers to organize and filter tests: + +### Primary Categories +- `@pytest.mark.unit` - Fast, isolated unit tests +- `@pytest.mark.integration` - Integration tests with external dependencies +- `@pytest.mark.slow` - Tests that take longer than 1 second + +### Functional Categories +- `@pytest.mark.cli` - Command-line interface tests +- `@pytest.mark.service` - Service layer tests +- `@pytest.mark.model` - Data model and structure tests +- `@pytest.mark.formatter` - Output formatting tests +- `@pytest.mark.parser` - Data parsing tests +- `@pytest.mark.validator` - Input validation tests + +### Special Categories +- `@pytest.mark.mock` - Tests that heavily use mocking +- `@pytest.mark.real_api` - Tests that make actual API calls +- `@pytest.mark.parametrized` - Parametrized tests with multiple scenarios +- `@pytest.mark.smoke` - Basic smoke tests for critical functionality +- `@pytest.mark.regression` - Regression tests for specific bug fixes + +## Running Tests + +### All Tests +```bash +pytest +``` + +### By Category +```bash +# Unit tests only +pytest -m unit + +# Integration tests only +pytest -m integration + +# Formatter tests only +pytest -m formatter + +# Exclude slow tests +pytest -m "not slow" + +# CLI tests only +pytest -m cli + +# Service layer tests +pytest -m service +``` + +### Coverage Reports +```bash +# Terminal coverage report +pytest --cov=toady + +# HTML coverage report +pytest --cov=toady --cov-report=html + +# Coverage with missing lines +pytest --cov=toady --cov-report=term-missing +``` + +## Test Configuration + +### pytest.ini +- Comprehensive marker definitions +- Coverage configuration +- Test discovery patterns +- Performance settings + +### pyproject.toml +- Additional pytest configuration +- Development dependencies +- Tool configuration for pytest plugins + +### conftest.py +- Shared fixtures for common test data +- Mock service configurations +- Test data generators +- Performance baselines + +## Fixtures + +### Session-scoped Fixtures +- `sample_datetime` - Consistent datetime for tests +- `sample_comment` - Reusable Comment instance +- `sample_review_thread` - Reusable ReviewThread instance +- `performance_baseline` - Performance expectations + +### Function-scoped Fixtures +- `runner` - Click CLI test runner +- `mock_gh_command` - GitHub CLI command mock +- `comment_factory` - Dynamic Comment creation +- `thread_factory` - Dynamic ReviewThread creation +- `test_data_generator` - Bulk test data generation + +### Service Mocks +- `mock_github_service` - GitHub API service mock +- `mock_fetch_service` - Fetch service mock +- `mock_reply_service` - Reply service mock +- `mock_resolve_service` - Resolve service mock + +## Best Practices + +### Test Organization +1. Use appropriate markers for all test classes +2. Group related tests in classes +3. Use descriptive test and class names +4. Keep tests focused and atomic + +### Performance +1. Use session-scoped fixtures for expensive setup +2. Mock external dependencies +3. Mark slow tests appropriately +4. Use test data generators for bulk data + +### Coverage +1. Aim for >80% code coverage +2. Focus on critical business logic +3. Test error conditions and edge cases +4. Exclude appropriate files from coverage + +### Mocking +1. Mock external services and APIs +2. Use realistic mock data +3. Test both success and failure scenarios +4. Verify mock interactions when appropriate + +## CI/CD Integration + +The test framework is integrated with: +- Pre-commit hooks for quality checks +- GitHub Actions for automated testing +- Coverage reporting and thresholds +- Code quality tools (black, ruff, mypy) + +## Example Usage + +```python +import pytest +from toady.models.models import ReviewThread + +@pytest.mark.model +@pytest.mark.unit +class TestReviewThread: + """Test the ReviewThread model.""" + + def test_creation(self, thread_factory): + """Test thread creation.""" + thread = thread_factory(status="RESOLVED") + assert thread.status == "RESOLVED" + + @pytest.mark.parametrize("status", ["RESOLVED", "UNRESOLVED"]) + def test_status_values(self, thread_factory, status): + """Test different status values.""" + thread = thread_factory(status=status) + assert thread.status == status +``` + +This framework provides a solid foundation for comprehensive testing that supports both development and CI/CD workflows. diff --git a/tests/conftest.py b/tests/conftest.py index 08d4d3a..8f65790 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,22 +1,21 @@ """Shared pytest fixtures and configuration.""" -import tempfile from datetime import datetime from pathlib import Path +import tempfile from unittest.mock import MagicMock, Mock -import pytest from click.testing import CliRunner +import pytest from toady.models.models import Comment, ReviewThread def pytest_configure(config): - """Configure pytest markers.""" - config.addinivalue_line( - "markers", - "integration: marks tests as integration tests (may require authentication)", - ) + """Configure pytest with additional settings and optimizations.""" + # Performance optimizations + if not hasattr(config.option, "tb"): + config.option.tb = "short" @pytest.fixture @@ -358,3 +357,51 @@ def mock_resolve_service(): } return service + + +# Utility fixtures for test data generation +@pytest.fixture +def test_data_generator(): + """Generator for creating varied test data sets.""" + + def _generate_test_threads(count=5, status="UNRESOLVED"): + """Generate a list of test threads.""" + threads = [] + for i in range(count): + thread = ReviewThread( + thread_id=f"RT_test_{i}", + title=f"Test thread {i}", + created_at=datetime(2024, 1, 15, 10, i, 0), + updated_at=datetime(2024, 1, 15, 10, i, 0), + status=status if isinstance(status, str) else status[i % len(status)], + author=f"test_user_{i}", + comments=[], + ) + threads.append(thread) + return threads + + _generate_test_threads.generate_test_comments = lambda count=3: [ + Comment( + comment_id=f"IC_test_{i}", + content=f"Test comment {i}", + author=f"comment_user_{i}", + created_at=datetime(2024, 1, 15, 10, i, 0), + updated_at=datetime(2024, 1, 15, 10, i, 0), + parent_id=None, + thread_id="RT_test_parent", + ) + for i in range(count) + ] + + return _generate_test_threads + + +@pytest.fixture(scope="session") +def performance_baseline(): + """Baseline performance expectations for testing.""" + return { + "max_format_time": 5.0, # seconds + "max_parse_time": 2.0, # seconds + "max_memory_mb": 100, # MB + "max_api_response_time": 10.0, # seconds + } diff --git a/tests/integration/cli/test_fetch_cli.py b/tests/integration/cli/test_fetch_cli.py index 573848b..0e97980 100644 --- a/tests/integration/cli/test_fetch_cli.py +++ b/tests/integration/cli/test_fetch_cli.py @@ -4,10 +4,13 @@ from unittest.mock import Mock, patch from click.testing import CliRunner +import pytest from toady.cli import cli +@pytest.mark.integration +@pytest.mark.cli class TestFetchCLI: """Test the fetch command CLI integration.""" @@ -378,11 +381,18 @@ def test_fetch_unexpected_error_handling( ) mock_service_class.return_value = mock_service - # Test pretty mode + # Test pretty mode without debug (no error details shown) result = runner.invoke(cli, ["fetch", "--pr", "123", "--pretty"]) assert result.exit_code == 1 assert "โŒ An unexpected error occurred" in result.output - assert "internal error" in result.output + assert "internal error" not in result.output + + # Test pretty mode with debug (error details shown) + with patch.dict("os.environ", {"TOADY_DEBUG": "1"}): + result = runner.invoke(cli, ["fetch", "--pr", "123", "--pretty"]) + assert result.exit_code == 1 + assert "โŒ An unexpected error occurred" in result.output + assert "internal error" in result.output # Test JSON mode result = runner.invoke(cli, ["fetch", "--pr", "123"]) diff --git a/tests/integration/cli/test_reply_cli.py b/tests/integration/cli/test_reply_cli.py index 434c769..7023a4c 100644 --- a/tests/integration/cli/test_reply_cli.py +++ b/tests/integration/cli/test_reply_cli.py @@ -455,8 +455,8 @@ def test_reply_enhanced_body_validation(self, runner: CliRunner) -> None: for body, expected_error in test_cases: result = runner.invoke(cli, ["reply", "--id", "123456789", "--body", body]) - assert result.exit_code == 2, f"Should fail for body: {repr(body)}" - assert expected_error in result.output, f"Expected error for {repr(body)}" + assert result.exit_code == 2, f"Should fail for body: {body!r}" + assert expected_error in result.output, f"Expected error for {body!r}" @patch("toady.commands.reply.ReplyService") def test_reply_with_verbose_mode_pretty( diff --git a/tests/integration/cli/test_resolve_cli.py b/tests/integration/cli/test_resolve_cli.py index 483c35a..9c07811 100644 --- a/tests/integration/cli/test_resolve_cli.py +++ b/tests/integration/cli/test_resolve_cli.py @@ -1,7 +1,7 @@ """Integration tests for the resolve CLI command.""" -import json from datetime import datetime +import json from unittest.mock import Mock, patch from click.testing import CliRunner diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py new file mode 100644 index 0000000..98925a3 --- /dev/null +++ b/tests/integration/conftest.py @@ -0,0 +1,473 @@ +"""Enhanced integration test configuration and fixtures for real API testing. + +This module provides comprehensive fixtures and utilities for integration tests +that interact with real GitHub APIs, including authentication management, +test data setup/cleanup, and resilience testing infrastructure. +""" + +from collections.abc import Generator +import os +from pathlib import Path +import subprocess +import tempfile +import time +from typing import Any + +from click.testing import CliRunner +import pytest + +from toady.services.github_service import GitHubService + + +@pytest.fixture(scope="session") +def integration_test_config() -> dict[str, Any]: + """Load integration test configuration from environment variables. + + Returns: + Dictionary containing integration test configuration parameters. + """ + # Load .env file for local development (CI uses environment variables directly) + try: + from dotenv import load_dotenv + + # Loads .env from current directory, doesn't override existing env vars + load_dotenv() + except ImportError: + pass # dotenv not available, skip gracefully + + return { + "test_repo": os.getenv("TOADY_TEST_REPO", "toady-test/integration-testing"), + "test_org": os.getenv("TOADY_TEST_ORG", "toady-test"), + "api_timeout": int(os.getenv("TOADY_API_TIMEOUT", "30")), + "rate_limit_buffer": int(os.getenv("TOADY_RATE_LIMIT_BUFFER", "100")), + "skip_slow_tests": os.getenv("TOADY_SKIP_SLOW_TESTS", "false").lower() + == "true", + "test_pr_number": int(os.getenv("TOADY_TEST_PR_NUMBER", "1")), + "max_retry_attempts": int(os.getenv("TOADY_MAX_RETRY_ATTEMPTS", "3")), + "retry_delay": float(os.getenv("TOADY_RETRY_DELAY", "1.0")), + } + + +@pytest.fixture(scope="session") +def github_api_health_check(integration_test_config: dict[str, Any]) -> bool: + """Verify GitHub API accessibility and authentication before running tests. + + Args: + integration_test_config: Integration test configuration. + + Returns: + True if GitHub API is accessible and authenticated. + + Raises: + pytest.skip: If GitHub API is not accessible or authentication fails. + """ + import json + + try: + # Check if gh CLI is available + result = subprocess.run( + ["gh", "--version"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + pytest.skip("GitHub CLI (gh) not available") + + # Check authentication status + auth_result = subprocess.run( + ["gh", "auth", "status"], + capture_output=True, + text=True, + timeout=10, + ) + if auth_result.returncode != 0: + pytest.skip("GitHub CLI not authenticated - run 'gh auth login'") + + # Check rate limit status + rate_limit_result = subprocess.run( + ["gh", "api", "rate_limit"], + capture_output=True, + text=True, + timeout=10, + ) + if rate_limit_result.returncode != 0: + pytest.skip("Unable to check GitHub API rate limits") + + rate_data = json.loads(rate_limit_result.stdout) + remaining = rate_data.get("rate", {}).get("remaining", 0) + + if remaining < integration_test_config["rate_limit_buffer"]: + pytest.skip(f"Insufficient GitHub API rate limit remaining: {remaining}") + + return True + + except subprocess.TimeoutExpired: + pytest.skip("GitHub CLI commands timed out") + except FileNotFoundError: + pytest.skip("GitHub CLI (gh) not found in PATH") + except json.JSONDecodeError: + pytest.skip("Invalid JSON response from GitHub API") + except Exception as e: + pytest.skip(f"GitHub API health check failed: {e}") + + +@pytest.fixture +def github_service_real(github_api_health_check: bool) -> GitHubService: + """Create a real GitHubService instance for integration testing. + + Args: + github_api_health_check: Ensures GitHub API is accessible. + + Returns: + Configured GitHubService instance. + """ + return GitHubService(timeout=30) + + +@pytest.fixture +def integration_cli_runner() -> CliRunner: + """Create a CLI runner configured for integration testing. + + Returns: + CliRunner instance with integration test configuration. + """ + runner = CliRunner() + return runner + + +@pytest.fixture +def temp_cache_dir() -> Generator[Path, None, None]: + """Create a temporary directory for cache during integration tests. + + Yields: + Path to temporary cache directory. + """ + with tempfile.TemporaryDirectory(prefix="toady_integration_cache_") as temp_dir: + yield Path(temp_dir) + + +@pytest.fixture +def test_repository_info(integration_test_config: dict[str, Any]) -> dict[str, Any]: + """Get information about the test repository. + + Args: + integration_test_config: Integration test configuration. + + Returns: + Dictionary containing test repository information. + """ + import subprocess + + repo = integration_test_config["test_repo"] + parts = repo.split("/") + + if len(parts) != 2: + pytest.skip(f"Invalid test repository format: {repo}") + + # Check if the test repository exists and is accessible + try: + result = subprocess.run( + ["gh", "api", f"repos/{repo}"], + capture_output=True, + text=True, + timeout=10, + ) + if result.returncode != 0: + pytest.skip( + f"Test repository {repo} is not accessible or does not exist. " + f"Set TOADY_TEST_REPO environment variable to a valid repository " + f"for integration tests." + ) + except (subprocess.TimeoutExpired, FileNotFoundError): + pytest.skip( + f"Cannot verify test repository {repo} accessibility - " + f"GitHub CLI not available or timed out" + ) + + return { + "owner": parts[0], + "repo": parts[1], + "full_name": repo, + "pr_number": integration_test_config["test_pr_number"], + } + + +@pytest.fixture +def verify_test_pr_exists( + github_service_real: GitHubService, test_repository_info: dict[str, Any] +) -> dict[str, Any]: + """Verify that the test PR exists and is accessible. + + Args: + github_service_real: Real GitHub service instance. + test_repository_info: Test repository information. + + Returns: + PR information if accessible. + + Raises: + pytest.skip: If test PR is not accessible. + """ + try: + # Try to access the test PR to verify it exists and we have permissions + result = subprocess.run( + [ + "gh", + "api", + f"repos/{test_repository_info['full_name']}/pulls/{test_repository_info['pr_number']}", + ], + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode != 0: + pytest.skip( + f"Test PR {test_repository_info['pr_number']} not accessible " + f"in {test_repository_info['full_name']}" + ) + + import json + + pr_data = json.loads(result.stdout) + + return { + "number": pr_data["number"], + "title": pr_data["title"], + "state": pr_data["state"], + "node_id": pr_data["node_id"], + "html_url": pr_data["html_url"], + } + + except Exception as e: + pytest.skip(f"Failed to verify test PR: {e}") + + +@pytest.fixture +def rate_limit_aware_delay(): + """Add delay between API calls to respect rate limits.""" + + def delay(seconds: float = 1.0) -> None: + """Add a delay between API calls. + + Args: + seconds: Number of seconds to delay. + """ + time.sleep(seconds) + + return delay + + +@pytest.fixture +def api_retry_helper(integration_test_config: dict[str, Any]): + """Helper for retrying API calls with exponential backoff.""" + + def retry_api_call(func, *args, **kwargs): + """Retry an API call with exponential backoff. + + Args: + func: Function to call. + *args: Positional arguments for the function. + **kwargs: Keyword arguments for the function. + + Returns: + Result of the function call. + + Raises: + Exception: If all retry attempts fail. + """ + max_attempts = integration_test_config["max_retry_attempts"] + base_delay = integration_test_config["retry_delay"] + + for attempt in range(max_attempts): + try: + return func(*args, **kwargs) + except Exception: + if attempt == max_attempts - 1: + raise + + delay = base_delay * (2**attempt) + time.sleep(delay) + + # This should never be reached, but just in case + raise RuntimeError("Maximum retry attempts exceeded") + + return retry_api_call + + +@pytest.fixture +def network_simulation(): + """Utilities for simulating network conditions during testing.""" + + class NetworkSimulator: + """Simulate various network conditions for testing.""" + + @staticmethod + def simulate_timeout(timeout_seconds: float = 5.0): + """Simulate a network timeout. + + Args: + timeout_seconds: Timeout duration in seconds. + """ + time.sleep(timeout_seconds) + + @staticmethod + def simulate_slow_connection(delay_seconds: float = 2.0): + """Simulate a slow network connection. + + Args: + delay_seconds: Delay duration in seconds. + """ + time.sleep(delay_seconds) + + @staticmethod + def simulate_intermittent_failure(failure_rate: float = 0.3): + """Simulate intermittent network failures. + + Args: + failure_rate: Probability of failure (0.0 to 1.0). + + Returns: + True if operation should succeed, False if it should fail. + """ + import random + + return random.random() > failure_rate + + return NetworkSimulator() + + +@pytest.fixture +def performance_monitor(): + """Monitor performance metrics during integration tests.""" + + class PerformanceMonitor: + """Monitor and measure performance during tests.""" + + def __init__(self): + self.start_time = None + self.metrics = {} + + def start_timing(self, operation: str): + """Start timing an operation. + + Args: + operation: Name of the operation being timed. + """ + self.start_time = time.time() + self.current_operation = operation + + def stop_timing(self) -> float: + """Stop timing and record the duration. + + Returns: + Duration of the operation in seconds. + """ + if self.start_time is None: + raise ValueError("No timing operation in progress") + + duration = time.time() - self.start_time + self.metrics[self.current_operation] = duration + self.start_time = None + return duration + + def get_metrics(self) -> dict[str, float]: + """Get all recorded performance metrics. + + Returns: + Dictionary of operation names to durations. + """ + return self.metrics.copy() + + def assert_performance_threshold(self, operation: str, max_seconds: float): + """Assert that an operation completed within a time threshold. + + Args: + operation: Name of the operation to check. + max_seconds: Maximum allowed duration in seconds. + + Raises: + AssertionError: If operation exceeded the threshold. + """ + duration = self.metrics.get(operation) + if duration is None: + raise ValueError(f"No metrics recorded for operation: {operation}") + + assert duration <= max_seconds, ( + f"Operation '{operation}' took {duration:.2f}s, " + f"which exceeds threshold of {max_seconds:.2f}s" + ) + + return PerformanceMonitor() + + +@pytest.fixture +def integration_test_cleanup(): + """Cleanup helper for integration tests.""" + cleanup_tasks = [] + + def add_cleanup(func, *args, **kwargs): + """Add a cleanup task to be executed after the test. + + Args: + func: Cleanup function to call. + *args: Positional arguments for the cleanup function. + **kwargs: Keyword arguments for the cleanup function. + """ + cleanup_tasks.append((func, args, kwargs)) + + yield add_cleanup + + # Execute cleanup tasks in reverse order + for func, args, kwargs in reversed(cleanup_tasks): + try: + func(*args, **kwargs) + except Exception as e: + # Log cleanup errors but don't fail the test + print(f"Cleanup error: {e}") + + +@pytest.fixture +def skip_if_slow(integration_test_config: dict[str, Any]): + """Skip slow tests based on configuration.""" + if integration_test_config["skip_slow_tests"]: + pytest.skip("Slow tests are disabled (TOADY_SKIP_SLOW_TESTS=true)") + + +# Session-level fixtures for expensive setup +@pytest.fixture(scope="session") +def session_github_service(github_api_health_check: bool) -> GitHubService: + """Create a session-level GitHub service instance.""" + return GitHubService(timeout=30) + + +@pytest.fixture(scope="session") +def session_test_repository_access( + session_github_service: GitHubService, integration_test_config: dict[str, Any] +) -> dict[str, Any]: + """Verify session-level access to test repository.""" + repo = integration_test_config["test_repo"] + + try: + # Test basic repository access + result = subprocess.run( + ["gh", "api", f"repos/{repo}"], capture_output=True, text=True, timeout=10 + ) + + if result.returncode != 0: + pytest.skip(f"Cannot access test repository: {repo}") + + import json + + repo_data = json.loads(result.stdout) + + return { + "name": repo_data["name"], + "full_name": repo_data["full_name"], + "private": repo_data["private"], + "permissions": repo_data.get("permissions", {}), + } + + except Exception as e: + pytest.skip(f"Failed to verify repository access: {e}") diff --git a/tests/integration/real_api/README.md b/tests/integration/real_api/README.md new file mode 100644 index 0000000..6fa38bd --- /dev/null +++ b/tests/integration/real_api/README.md @@ -0,0 +1,279 @@ +# Real API Integration Tests + +This directory contains comprehensive integration tests that interact with actual GitHub APIs to verify end-to-end functionality of the toady CLI. + +## Overview + +These tests are designed to be **world-class integration tests** that provide confidence in real-world usage scenarios. They cover: + +- **End-to-end workflows**: Complete user journeys from fetch to reply to resolve +- **Authentication flows**: GitHub authentication and authorization patterns +- **Network resilience**: Rate limiting, timeouts, retry logic, and error recovery +- **Performance monitoring**: Benchmarking, scalability, and resource utilization + +## Test Categories + +### 1. End-to-End Workflows (`test_end_to_end_workflows.py`) +- Complete review cycle testing (fetch โ†’ reply โ†’ resolve) +- Bulk operations testing +- Error recovery workflows +- Interactive command testing +- Format consistency validation + +### 2. Authentication Flows (`test_authentication_flows.py`) +- User identity verification +- Repository access permissions +- Rate limit status monitoring +- Token scope validation +- Cross-organization access patterns +- Permission boundary testing + +### 3. Network Resilience (`test_network_resilience.py`) +- API timeout handling +- Network retry behavior +- Large response processing +- Concurrent usage patterns +- Progressive backoff testing +- Connection recovery after failures +- Rate limit compliance and error handling + +### 4. Performance Monitoring (`test_performance_monitoring.py`) +- Performance baseline establishment +- Memory usage pattern analysis +- Large PR performance testing +- Concurrent operation impact +- Cache effectiveness measurement +- Network latency impact assessment +- Scalability pattern validation +- Resource utilization monitoring + +## Environment Setup + +### Prerequisites + +1. **GitHub CLI Authentication**: + ```bash + gh auth login + gh auth status # Verify authentication + ``` + +2. **Test Repository Access**: + Set up environment variables for test repository: + ```bash + export TOADY_TEST_REPO="your-org/test-repo" + export TOADY_TEST_PR_NUMBER="1" + ``` + +3. **Optional Configuration**: + ```bash + export TOADY_API_TIMEOUT="30" + export TOADY_RATE_LIMIT_BUFFER="100" + export TOADY_SKIP_SLOW_TESTS="false" + export TOADY_MAX_RETRY_ATTEMPTS="3" + export TOADY_RETRY_DELAY="1.0" + ``` + +### Test Repository Requirements + +The test repository should have: +- At least one pull request with review comments +- Appropriate permissions for the authenticated user +- Active review threads (both resolved and unresolved) + +## Running the Tests + +### Run All Real API Tests +```bash +pytest tests/integration/real_api/ -m real_api +``` + +### Run by Category +```bash +# End-to-end workflows only +pytest tests/integration/real_api/test_end_to_end_workflows.py + +# Authentication tests only +pytest tests/integration/real_api/test_authentication_flows.py -m auth + +# Network resilience tests only +pytest tests/integration/real_api/test_network_resilience.py -m resilience + +# Performance tests only +pytest tests/integration/real_api/test_performance_monitoring.py -m performance +``` + +### Run with Specific Markers +```bash +# Fast tests only (exclude slow tests) +pytest tests/integration/real_api/ -m "real_api and not slow" + +# Slow tests only +pytest tests/integration/real_api/ -m "slow" + +# Authentication-related tests +pytest tests/integration/real_api/ -m "auth" + +# Resilience and error handling tests +pytest tests/integration/real_api/ -m "resilience" + +# Performance and scalability tests +pytest tests/integration/real_api/ -m "performance" +``` + +### Verbose Output with Performance Monitoring +```bash +pytest tests/integration/real_api/ -m real_api -v -s --tb=short +``` + +## Test Configuration + +### Available Pytest Markers + +- `@pytest.mark.real_api`: Tests that make actual GitHub API calls +- `@pytest.mark.slow`: Tests that take significant time to complete +- `@pytest.mark.auth`: Authentication and authorization tests +- `@pytest.mark.resilience`: Network resilience and error recovery tests +- `@pytest.mark.performance`: Performance and scalability tests + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `TOADY_TEST_REPO` | `toady-test/integration-testing` | GitHub repository for testing | +| `TOADY_TEST_PR_NUMBER` | `1` | PR number to use for testing | +| `TOADY_API_TIMEOUT` | `30` | API call timeout in seconds | +| `TOADY_RATE_LIMIT_BUFFER` | `100` | Minimum rate limit remaining for tests | +| `TOADY_SKIP_SLOW_TESTS` | `false` | Skip slow-running tests | +| `TOADY_MAX_RETRY_ATTEMPTS` | `3` | Maximum retry attempts for failed API calls | +| `TOADY_RETRY_DELAY` | `1.0` | Base delay between retry attempts | + +## Test Features + +### Smart Test Skipping +Tests automatically skip when: +- GitHub CLI is not authenticated +- Test repository is not accessible +- Rate limits are too low +- Required test data is not available + +### Performance Monitoring +Tests include built-in performance monitoring: +- Operation timing and benchmarking +- Memory usage tracking +- Rate limit awareness +- Resource utilization monitoring + +### Error Recovery +Tests validate error recovery patterns: +- Transient error handling +- Rate limit compliance +- Network timeout recovery +- Authentication error handling + +### Data Consistency +Tests ensure data consistency: +- Multiple fetch operations return consistent results +- State changes persist across operations +- Cache behavior is correct + +## CI/CD Integration + +### Running in Continuous Integration + +For CI environments, use: +```bash +# Skip slow tests in CI +pytest tests/integration/real_api/ -m "real_api and not slow" --maxfail=5 + +# Or skip real API tests entirely if no auth available +pytest tests/integration/ -m "not real_api" +``` + +### GitHub Actions Example +```yaml +- name: Run Integration Tests + env: + TOADY_TEST_REPO: ${{ secrets.TEST_REPO }} + TOADY_SKIP_SLOW_TESTS: "true" + run: | + gh auth login --with-token <<< "${{ secrets.GITHUB_TOKEN }}" + pytest tests/integration/real_api/ -m "real_api and not slow" +``` + +## Debugging and Troubleshooting + +### Common Issues + +1. **Authentication Errors**: + ```bash + gh auth status + gh auth refresh + ``` + +2. **Rate Limit Issues**: + ```bash + gh api rate_limit + # Wait for rate limit reset or increase TOADY_RATE_LIMIT_BUFFER + ``` + +3. **Test Repository Access**: + ```bash + gh api repos/$TOADY_TEST_REPO + gh pr list --repo $TOADY_TEST_REPO + ``` + +4. **Slow Test Performance**: + ```bash + export TOADY_SKIP_SLOW_TESTS=true + pytest tests/integration/real_api/ -m "not slow" + ``` + +### Verbose Debugging +```bash +pytest tests/integration/real_api/ -v -s --tb=long --capture=no +``` + +### Performance Analysis +```bash +pytest tests/integration/real_api/ -m performance --durations=0 +``` + +## Best Practices + +### For Test Development + +1. **Always use rate limiting aware delays** +2. **Include proper cleanup for any data created** +3. **Handle authentication and permission errors gracefully** +4. **Include performance assertions where appropriate** +5. **Use descriptive test names and documentation** + +### For Test Execution + +1. **Check authentication before running tests** +2. **Verify test repository access** +3. **Monitor rate limit usage** +4. **Run slow tests separately when needed** +5. **Use appropriate environment configuration** + +## Contributing + +When adding new integration tests: + +1. Follow the existing patterns and structure +2. Use appropriate pytest markers +3. Include proper error handling and cleanup +4. Add performance monitoring where relevant +5. Update this README if adding new categories +6. Ensure tests work with the provided test fixtures + +## Test Quality Metrics + +These integration tests aim to achieve: +- **Comprehensive coverage** of real-world usage scenarios +- **Robust error handling** for all failure modes +- **Performance validation** for acceptable response times +- **Reliability testing** under various network conditions +- **Security verification** of authentication flows + +The tests serve as both validation and documentation of the system's behavior under real conditions. diff --git a/tests/integration/real_api/__init__.py b/tests/integration/real_api/__init__.py new file mode 100644 index 0000000..c2eb5b0 --- /dev/null +++ b/tests/integration/real_api/__init__.py @@ -0,0 +1,5 @@ +"""Real API integration tests for toady CLI. + +This package contains comprehensive integration tests that interact with +actual GitHub APIs to verify end-to-end functionality. +""" diff --git a/tests/integration/real_api/test_authentication_flows.py b/tests/integration/real_api/test_authentication_flows.py new file mode 100644 index 0000000..48c6395 --- /dev/null +++ b/tests/integration/real_api/test_authentication_flows.py @@ -0,0 +1,462 @@ +"""Integration tests for authentication and authorization flows. + +These tests verify proper handling of GitHub authentication scenarios, +permission boundaries, and error conditions related to access control. +""" + +import subprocess +from typing import Any + +from click.testing import CliRunner +import pytest + +from toady.cli import cli +from toady.services.github_service import GitHubService + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.auth +class TestAuthenticationFlows: + """Test GitHub authentication and authorization scenarios.""" + + def test_authenticated_user_identity_verification( + self, + github_service_real: GitHubService, + integration_cli_runner: CliRunner, + ): + """Verify that the authenticated user identity is correctly detected.""" + # Get current user info via GitHub API + try: + result = subprocess.run( + ["gh", "api", "user"], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, "Failed to get user info from GitHub API" + + import json + + user_data = json.loads(result.stdout) + + # Verify we have essential user information + assert "login" in user_data + assert "id" in user_data + assert user_data["login"] is not None + assert user_data["id"] is not None + + # Store for other tests to use + self.current_user_login = user_data["login"] + + except Exception as e: + pytest.fail(f"Could not verify user identity: {e}") + + def test_repository_access_permissions( + self, + test_repository_info: dict[str, Any], + integration_cli_runner: CliRunner, + rate_limit_aware_delay, + ): + """Test access to repository with current user permissions.""" + repo_full_name = test_repository_info["full_name"] + + # Check repository permissions + try: + result = subprocess.run( + ["gh", "api", f"repos/{repo_full_name}"], + capture_output=True, + text=True, + timeout=10, + ) + + assert result.returncode == 0, f"Cannot access repository {repo_full_name}" + + import json + + repo_data = json.loads(result.stdout) + + # Verify basic repository information + assert repo_data["full_name"] == repo_full_name + + # Check permissions if available + permissions = repo_data.get("permissions", {}) + + # For integration testing, we need at least read permissions + if permissions: + assert permissions.get( + "pull", False + ), "Need pull permissions for testing" + + except Exception as e: + pytest.fail(f"Repository access verification failed: {e}") + + def test_pr_access_with_current_permissions( + self, + verify_test_pr_exists: dict[str, Any], + test_repository_info: dict[str, Any], + integration_cli_runner: CliRunner, + ): + """Test accessing PR data with current user permissions.""" + pr_number = verify_test_pr_exists["number"] + test_repository_info["full_name"] + + # Test fetch command with real authentication + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + if result.exit_code != 0: + # If fetch fails, it might be due to permissions + if ( + "permission" in result.output.lower() + or "access" in result.output.lower() + ): + pytest.skip(f"Insufficient permissions to access PR {pr_number}") + else: + pytest.fail(f"Fetch failed for other reason: {result.output}") + + # If successful, verify we got data + import json + + try: + threads_data = json.loads(result.output) + assert isinstance(threads_data, list) + except json.JSONDecodeError: + pytest.fail(f"Invalid JSON response: {result.output}") + + def test_rate_limit_status_and_handling( + self, + github_service_real: GitHubService, + integration_test_config: dict[str, Any], + ): + """Test rate limit status checking and handling.""" + try: + # Check current rate limit status + result = subprocess.run( + ["gh", "api", "rate_limit"], capture_output=True, text=True, timeout=10 + ) + + assert result.returncode == 0, "Failed to check rate limit status" + + import json + + rate_data = json.loads(result.stdout) + + # Verify rate limit structure + assert "rate" in rate_data + rate_info = rate_data["rate"] + + assert "limit" in rate_info + assert "remaining" in rate_info + assert "reset" in rate_info + + # Verify we have sufficient remaining calls for testing + remaining = rate_info["remaining"] + buffer = integration_test_config["rate_limit_buffer"] + + if remaining < buffer: + pytest.skip( + f"Insufficient rate limit remaining: {remaining} < {buffer}" + ) + + # Log rate limit info for debugging + print(f"Rate limit: {remaining}/{rate_info['limit']} remaining") + + except Exception as e: + pytest.fail(f"Rate limit check failed: {e}") + + def test_token_scope_verification(self): + """Verify that the GitHub token has appropriate scopes for testing.""" + try: + # Check token scopes via API + result = subprocess.run( + ["gh", "api", "user", "-i"], # Include headers + capture_output=True, + text=True, + timeout=10, + ) + + if result.returncode != 0: + pytest.skip("Could not verify token scopes") + + # Parse headers to find X-OAuth-Scopes + headers = result.stdout if result.stdout else "" + oauth_scopes = None + + for line in headers.split("\n"): + if line.lower().startswith("x-oauth-scopes:"): + oauth_scopes = line.split(":", 1)[1].strip() + break + + if oauth_scopes: + scopes = [scope.strip() for scope in oauth_scopes.split(",")] + + # Check for essential scopes + # Note: repo scope is usually needed for PR operations + if "repo" not in scopes and "public_repo" not in scopes: + pytest.skip("Token lacks repository access scopes") + + print(f"Token scopes: {oauth_scopes}") + else: + # If we can't detect scopes, proceed anyway + print("Could not detect token scopes from headers") + + except Exception as e: + # Non-critical for most tests + print(f"Token scope verification failed: {e}") + + def test_unauthenticated_behavior_simulation( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + ): + """Simulate unauthenticated behavior by temporarily removing auth.""" + verify_test_pr_exists["number"] + + # This test is complex because we need to temporarily remove auth + # We'll do a simple check to ensure error handling works + + # Skip this test until we can properly simulate unauthenticated state + # The gh CLI caches auth tokens and makes it difficult to test + # unauthenticated scenarios without complex environment manipulation + pytest.skip( + "Unauthenticated behavior simulation requires complex gh auth manipulation" + ) + + def test_cross_organization_access_patterns( + self, + test_repository_info: dict[str, Any], + integration_cli_runner: CliRunner, + ): + """Test access patterns across different organizations (if applicable).""" + repo_full_name = test_repository_info["full_name"] + owner, repo_name = repo_full_name.split("/") + + # Check if we can access organization information + try: + org_result = subprocess.run( + ["gh", "api", f"orgs/{owner}"], + capture_output=True, + text=True, + timeout=10, + ) + + if org_result.returncode == 0: + # This is an organization + import json + + org_data = json.loads(org_result.stdout) + + assert org_data["login"] == owner + print(f"Testing with organization: {owner}") + + # Test organization-level permissions + # (This would be expanded based on specific org policies) + + else: + # This might be a user account, not an organization + user_result = subprocess.run( + ["gh", "api", f"users/{owner}"], + capture_output=True, + text=True, + timeout=10, + ) + + if user_result.returncode == 0: + print(f"Testing with user account: {owner}") + else: + pytest.skip(f"Could not determine owner type for {owner}") + + except Exception as e: + pytest.skip(f"Could not test cross-organization access: {e}") + + def test_permission_boundary_validation( + self, + verify_test_pr_exists: dict[str, Any], + test_repository_info: dict[str, Any], + integration_cli_runner: CliRunner, + ): + """Test operations at the boundary of user permissions.""" + pr_number = verify_test_pr_exists["number"] + + # Test operations that require different permission levels + + # 1. Read operations (should work with minimal permissions) + read_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + # Read should generally work if we can access the PR at all + if read_result.exit_code != 0: + # If read fails, note why + print(f"Read operation failed: {read_result.output}") + + # 2. Write operations (require higher permissions) + # We'll test with a carefully chosen approach to avoid spam + + # First, check if we have any existing threads to work with + if read_result.exit_code == 0: + import json + + threads = json.loads(read_result.output) + + if threads and len(threads) > 0: + # Test resolving a thread (if we have permission) + thread_id = threads[0]["thread_id"] + + resolve_result = integration_cli_runner.invoke( + cli, ["resolve", "--thread-id", thread_id, "--format", "json"] + ) + + if resolve_result.exit_code == 0: + print("User has thread resolution permissions") + + # Undo the resolution to clean up + undo_result = integration_cli_runner.invoke( + cli, + [ + "resolve", + "--thread-id", + thread_id, + "--undo", + "--format", + "json", + ], + ) + + if undo_result.exit_code != 0: + print(f"Could not undo resolution: {undo_result.output}") + + else: + print( + f"User lacks thread resolution permissions: " + f"{resolve_result.output}" + ) + + def test_github_enterprise_compatibility_detection(self): + """Test detection of GitHub Enterprise vs GitHub.com.""" + try: + # Check GitHub CLI configuration + subprocess.run( + ["gh", "config", "get", "git_protocol"], + capture_output=True, + text=True, + timeout=10, + ) + + # Check the API endpoint being used + # For GitHub Enterprise, this would be different + api_result = subprocess.run( + ["gh", "api", "meta"], capture_output=True, text=True, timeout=10 + ) + + if api_result.returncode == 0: + import json + + meta_data = json.loads(api_result.stdout) + + # GitHub.com returns specific meta information + if "github_services_sha" in meta_data: + print("Testing against GitHub.com") + else: + print("Possibly testing against GitHub Enterprise") + + # Note: More specific Enterprise detection would require + # checking the hostname configuration in gh CLI + + except Exception as e: + print(f"Could not detect GitHub instance type: {e}") + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.auth +class TestAuthenticationErrorHandling: + """Test proper handling of authentication-related errors.""" + + def test_graceful_handling_of_auth_errors( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + ): + """Test that authentication errors are handled gracefully.""" + verify_test_pr_exists["number"] + + # This is a conceptual test - in practice, it's difficult to simulate + # auth errors without actually breaking authentication + + # We can test the error messages our commands would produce + # by looking at command output for certain failure modes + + # Test with potentially invalid data that might trigger auth checks + invalid_result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + "999999999", # Very unlikely to exist or be accessible + "--format", + "json", + ], + ) + + # Should fail, but with a proper error message (not crash) + assert invalid_result.exit_code != 0 + + # Error message should be informative + error_output = invalid_result.output.lower() + assert len(error_output) > 0 + + # Should not contain sensitive information or stack traces in production + assert "traceback" not in error_output or "exception" not in error_output + + def test_token_expiration_handling_simulation(self): + """Test handling of expired tokens (simulated).""" + # This would require a more sophisticated test setup + # to actually simulate token expiration + + # For now, we verify that our error handling code paths exist + # and would handle such scenarios appropriately + + try: + # Check if we can detect token validity + result = subprocess.run( + ["gh", "auth", "status"], capture_output=True, text=True, timeout=10 + ) + + if result.returncode == 0: + print("Current token is valid") + else: + print("Token appears to be invalid or expired") + # This would be where we test the error handling + + except Exception as e: + print(f"Token validation check failed: {e}") + + def test_network_authentication_resilience( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + api_retry_helper, + ): + """Test authentication resilience under network conditions.""" + pr_number = verify_test_pr_exists["number"] + + # Test that authentication works consistently across multiple calls + def make_authenticated_call(): + return integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + # Make multiple calls to test consistency + for i in range(3): + result = api_retry_helper(make_authenticated_call) + + # Each call should either succeed or fail consistently + # (not randomly based on auth state) + if i == 0: + first_exit_code = result.exit_code + else: + assert result.exit_code == first_exit_code, ( + f"Inconsistent auth behavior: call {i+1} had exit code " + f"{result.exit_code}, but first call had {first_exit_code}" + ) diff --git a/tests/integration/real_api/test_end_to_end_workflows.py b/tests/integration/real_api/test_end_to_end_workflows.py new file mode 100644 index 0000000..18e0a74 --- /dev/null +++ b/tests/integration/real_api/test_end_to_end_workflows.py @@ -0,0 +1,507 @@ +"""End-to-end integration tests for complete GitHub review workflows. + +These tests verify complete user workflows against real GitHub repositories, +testing the integration between fetch, reply, and resolve commands. +""" + +import json +import subprocess +import time +from typing import Any + +from click.testing import CliRunner +import pytest + +from toady.cli import cli + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.slow +class TestEndToEndWorkflows: + """Test complete end-to-end review workflows against real GitHub API.""" + + def test_complete_review_cycle_workflow( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + test_repository_info: dict[str, Any], + rate_limit_aware_delay, + performance_monitor, + integration_test_cleanup, + ): + """Test a complete review cycle: fetch -> reply -> resolve workflow. + + This test verifies that a user can: + 1. Fetch unresolved review threads from a PR + 2. Reply to a review comment + 3. Resolve the review thread + 4. Verify the thread is marked as resolved + """ + pr_number = verify_test_pr_exists["number"] + repo_full_name = test_repository_info["full_name"] + + # Step 1: Fetch unresolved review threads + performance_monitor.start_timing("fetch_unresolved_threads") + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + performance_monitor.stop_timing() + + raw_out = getattr(result, "stdout", result.output) + assert result.exit_code == 0, f"Fetch command failed: {raw_out}" + + try: + threads_data = json.loads(raw_out) + except json.JSONDecodeError: + pytest.fail(f"Invalid JSON response from fetch: {raw_out}") + + assert isinstance(threads_data, list), "Expected list of threads" + + # If no unresolved threads, create a test comment first + if not threads_data: + # Create a test review comment for this test + test_comment_body = f"Integration test comment - {int(time.time())}" + + create_result = subprocess.run( + [ + "gh", + "pr", + "comment", + str(pr_number), + "--repo", + repo_full_name, + "--body", + test_comment_body, + ], + capture_output=True, + text=True, + timeout=30, + ) + + if create_result.returncode != 0: + pytest.skip("Could not create test comment for workflow test") + + # Add cleanup for the test comment + integration_test_cleanup( + self._cleanup_comment, create_result.stdout.strip() + ) + + # Wait for comment to be processed + rate_limit_aware_delay(2.0) + + # Re-fetch to get the new comment + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + raw_out = getattr(result, "stdout", result.output) + assert result.exit_code == 0 + threads_data = json.loads(raw_out) + + if not threads_data: + pytest.skip("No review threads available for end-to-end testing") + + # Get the first thread for testing + test_thread = threads_data[0] + + # Verify thread structure + assert "thread_id" in test_thread + assert "comments" in test_thread + assert len(test_thread["comments"]) > 0 + + thread_id = test_thread["thread_id"] + first_comment = test_thread["comments"][0] + comment_id = first_comment["comment_id"] + + # Step 2: Reply to the review comment + performance_monitor.start_timing("post_reply") + + reply_body = f"Integration test reply - {int(time.time())}" + + reply_result = integration_cli_runner.invoke( + cli, + [ + "reply", + "--comment-id", + comment_id, + "--body", + reply_body, + "--format", + "json", + ], + ) + + performance_monitor.stop_timing() + rate_limit_aware_delay() + + assert ( + reply_result.exit_code == 0 + ), f"Reply command failed: {reply_result.output}" + + try: + reply_data = json.loads(reply_result.output) + except json.JSONDecodeError: + pytest.fail(f"Invalid JSON response from reply: {reply_result.output}") + + assert reply_data.get("success") is True + assert "comment_id" in reply_data + + # Step 3: Resolve the review thread + performance_monitor.start_timing("resolve_thread") + + resolve_result = integration_cli_runner.invoke( + cli, ["resolve", "--thread-id", thread_id, "--format", "json"] + ) + + performance_monitor.stop_timing() + rate_limit_aware_delay() + + assert ( + resolve_result.exit_code == 0 + ), f"Resolve command failed: {resolve_result.output}" + + try: + resolve_data = json.loads(resolve_result.output) + except json.JSONDecodeError: + pytest.fail(f"Invalid JSON response from resolve: {resolve_result.output}") + + assert resolve_data.get("success") is True + assert resolve_data.get("thread_id") == thread_id + assert resolve_data.get("resolved") is True + + # Step 4: Verify thread is resolved by fetching again + performance_monitor.start_timing("verify_resolution") + + verify_result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + str(pr_number), + "--resolved", # Include resolved threads + "--format", + "json", + ], + ) + + performance_monitor.stop_timing() + + assert verify_result.exit_code == 0 + + resolved_threads = json.loads(verify_result.output) + resolved_thread = next( + (t for t in resolved_threads if t["thread_id"] == thread_id), None + ) + + assert resolved_thread is not None, "Resolved thread not found in results" + assert resolved_thread.get("status") == "RESOLVED" + + # Performance assertions + performance_monitor.assert_performance_threshold( + "fetch_unresolved_threads", 10.0 + ) + performance_monitor.assert_performance_threshold("post_reply", 15.0) + performance_monitor.assert_performance_threshold("resolve_thread", 10.0) + performance_monitor.assert_performance_threshold("verify_resolution", 10.0) + + def test_bulk_thread_resolution_workflow( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + test_repository_info: dict[str, Any], + rate_limit_aware_delay, + performance_monitor, + skip_if_slow, + ): + """Test bulk resolution of all threads in a PR.""" + pr_number = verify_test_pr_exists["number"] + + # First, fetch all unresolved threads + performance_monitor.start_timing("fetch_for_bulk_resolve") + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + performance_monitor.stop_timing() + + assert result.exit_code == 0 + + threads_data = json.loads(result.output) + unresolved_count = len(threads_data) + + if unresolved_count == 0: + pytest.skip("No unresolved threads available for bulk resolution test") + + # Perform bulk resolution + performance_monitor.start_timing("bulk_resolve_all") + + bulk_result = integration_cli_runner.invoke( + cli, ["resolve", "--all", "--pr", str(pr_number), "--format", "json"] + ) + + performance_monitor.stop_timing() + rate_limit_aware_delay(2.0) # Allow more time for bulk operations + + assert bulk_result.exit_code == 0, f"Bulk resolve failed: {bulk_result.output}" + + bulk_data = json.loads(bulk_result.output) + assert bulk_data.get("success") is True + assert bulk_data.get("resolved_count") == unresolved_count + + # Verify all threads are now resolved + verify_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + assert verify_result.exit_code == 0 + remaining_unresolved = json.loads(verify_result.output) + + # Should be no unresolved threads remaining + assert len(remaining_unresolved) == 0, ( + f"Expected 0 unresolved threads after bulk resolve, " + f"but found {len(remaining_unresolved)}" + ) + + # Performance assertion for bulk operations + performance_monitor.assert_performance_threshold("bulk_resolve_all", 30.0) + + def test_error_recovery_workflow( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + test_repository_info: dict[str, Any], + rate_limit_aware_delay, + ): + """Test error recovery in workflows (invalid IDs, network issues, etc.).""" + verify_test_pr_exists["number"] + + # Test 1: Invalid comment ID handling + invalid_reply_result = integration_cli_runner.invoke( + cli, + [ + "reply", + "--comment-id", + "INVALID_COMMENT_ID_12345", + "--body", + "Test reply", + "--format", + "json", + ], + ) + + # Should fail gracefully with proper error message + assert invalid_reply_result.exit_code != 0 + + # Test 2: Invalid thread ID handling + invalid_resolve_result = integration_cli_runner.invoke( + cli, + ["resolve", "--thread-id", "INVALID_THREAD_ID_12345", "--format", "json"], + ) + + # Should fail gracefully with proper error message + assert invalid_resolve_result.exit_code != 0 + + # Test 3: Invalid PR number handling + invalid_fetch_result = integration_cli_runner.invoke( + cli, + ["fetch", "--pr", "999999", "--format", "json"], # Very unlikely to exist + ) + + # Should fail gracefully with proper error message + assert invalid_fetch_result.exit_code != 0 + + def test_interactive_workflow_with_multiple_prs( + self, + integration_cli_runner: CliRunner, + test_repository_info: dict[str, Any], + rate_limit_aware_delay, + skip_if_slow, + ): + """Test interactive workflow that handles multiple PRs.""" + test_repository_info["full_name"] + + # Fetch without specifying PR (interactive mode) + result = integration_cli_runner.invoke( + cli, ["fetch", "--format", "json"], input="\n" + ) # Just press enter to select first PR + + # Should either work with available PRs or skip gracefully + if result.exit_code == 0: + # Verify we got valid thread data + # For JSON format, check stdout only (stderr contains interactive messages) + try: + raw_out = getattr(result, "stdout", result.output) + threads_data = json.loads(raw_out) + assert isinstance(threads_data, list) + except json.JSONDecodeError: + raw_out = getattr(result, "stdout", result.output) + pytest.fail(f"Invalid JSON response in output: {raw_out}") + else: + # Interactive mode might fail if no PRs available - that's okay + assert ( + "No pull requests found" in result.output + or "error" in result.output.lower() + ) + + def test_format_consistency_across_workflow( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + rate_limit_aware_delay, + ): + """Test that output formats are consistent across all commands in a workflow.""" + pr_number = verify_test_pr_exists["number"] + + # Test JSON format consistency + json_commands = [ + (["fetch", "--pr", str(pr_number), "--format", "json"], "fetch"), + ( + ["fetch", "--pr", str(pr_number), "--resolved", "--format", "json"], + "fetch_resolved", + ), + ] + + for cmd_args, cmd_name in json_commands: + result = integration_cli_runner.invoke(cli, cmd_args) + + assert result.exit_code == 0, f"{cmd_name} failed: {result.output}" + + try: + data = json.loads(result.output) + assert isinstance(data, list), f"{cmd_name} should return a list" + except json.JSONDecodeError: + pytest.fail(f"{cmd_name} returned invalid JSON: {result.output}") + + rate_limit_aware_delay() + + # Test pretty format consistency + pretty_commands = [ + (["fetch", "--pr", str(pr_number), "--format", "pretty"], "fetch_pretty"), + ] + + for cmd_args, cmd_name in pretty_commands: + result = integration_cli_runner.invoke(cli, cmd_args) + + assert result.exit_code == 0, f"{cmd_name} failed: {result.output}" + + # Pretty format should contain human-readable content + assert len(result.output.strip()) > 0 + # Should not be JSON + assert not result.output.strip().startswith("[") + assert not result.output.strip().startswith("{") + + rate_limit_aware_delay() + + def _cleanup_comment(self, comment_url: str): + """Clean up a test comment after the test. + + Args: + comment_url: URL of the comment to delete. + """ + import contextlib + + with contextlib.suppress(Exception): + # Extract comment ID from URL and delete + # This is a best-effort cleanup - if it fails, it's not critical + subprocess.run( + ["gh", "api", "--method", "DELETE", comment_url], + capture_output=True, + timeout=10, + ) + + +@pytest.mark.integration +@pytest.mark.real_api +class TestWorkflowEdgeCases: + """Test edge cases and boundary conditions in workflows.""" + + def test_workflow_with_empty_pr( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + ): + """Test workflow behavior with a PR that has no review comments.""" + pr_number = verify_test_pr_exists["number"] + + # Fetch from PR (might have no comments) + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + assert result.exit_code == 0 + + threads_data = json.loads(result.output) + + # Should return empty list if no comments + assert isinstance(threads_data, list) + # Length can be 0 or more - both are valid states + + def test_workflow_state_persistence( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + rate_limit_aware_delay, + ): + """Test that workflow state changes persist across multiple commands.""" + pr_number = verify_test_pr_exists["number"] + + # Fetch current state + initial_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + assert initial_result.exit_code == 0 + initial_threads = json.loads(initial_result.output) + + rate_limit_aware_delay() + + # Fetch again - should get same results (assuming no external changes) + second_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + assert second_result.exit_code == 0 + second_threads = json.loads(second_result.output) + + # Thread count should be stable + assert len(initial_threads) == len(second_threads) + + # Thread IDs should be consistent + initial_ids = {t["thread_id"] for t in initial_threads} + second_ids = {t["thread_id"] for t in second_threads} + assert initial_ids == second_ids + + def test_concurrent_workflow_operations( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + rate_limit_aware_delay, + skip_if_slow, + ): + """Test behavior when multiple operations might be happening concurrently.""" + pr_number = verify_test_pr_exists["number"] + + # Perform multiple fetch operations in quick succession + results = [] + + for _i in range(3): + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + results.append(result) + rate_limit_aware_delay(0.5) # Small delay between requests + + # All should succeed + for i, result in enumerate(results): + assert result.exit_code == 0, f"Fetch {i+1} failed: {result.output}" + + # Should return valid JSON + try: + threads = json.loads(result.output) + assert isinstance(threads, list) + except json.JSONDecodeError: + pytest.fail(f"Fetch {i+1} returned invalid JSON: {result.output}") diff --git a/tests/integration/real_api/test_network_resilience.py b/tests/integration/real_api/test_network_resilience.py new file mode 100644 index 0000000..71ce97d --- /dev/null +++ b/tests/integration/real_api/test_network_resilience.py @@ -0,0 +1,533 @@ +"""Integration tests for network resilience and rate limiting. + +These tests verify proper handling of network conditions, rate limiting, +retry logic, and error recovery in real API interactions. +""" + +import json +import os +import subprocess +import time +from typing import Any + +from click.testing import CliRunner +import pytest + +from toady.cli import cli + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.resilience +@pytest.mark.slow +class TestNetworkResilience: + """Test network resilience and error recovery.""" + + def test_api_timeout_handling( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + ): + """Test handling of API timeouts.""" + pr_number = verify_test_pr_exists["number"] + + # Test with normal timeout first + performance_monitor.start_timing("normal_timeout_fetch") + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + normal_duration = performance_monitor.stop_timing() + + # Should complete within reasonable time + assert normal_duration < 30.0, f"Normal fetch took too long: {normal_duration}s" + + if result.exit_code == 0: + # Verify we got valid data + threads = json.loads(result.output) + assert isinstance(threads, list) + else: + # If it failed, ensure it's a proper error, not a timeout + assert "timeout" not in result.output.lower(), "Unexpected timeout error" + assert ( + normal_duration < 30.0 + ), "Operation finished but exceeded timeout threshold" + + def test_network_retry_behavior( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + api_retry_helper, + rate_limit_aware_delay, + ): + """Test retry behavior under network conditions.""" + pr_number = verify_test_pr_exists["number"] + + def fetch_with_potential_failure(): + """Make a fetch call that might need retrying.""" + return integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + # Use the retry helper to make the call + result = api_retry_helper(fetch_with_potential_failure) + + # Should eventually succeed or fail consistently + assert result.exit_code == 0, f"CLI call failed:\\n{result.output}" + + rate_limit_aware_delay() + + def test_large_response_handling( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + skip_if_slow, + ): + """Test handling of large API responses.""" + pr_number = verify_test_pr_exists["number"] + + # Fetch with resolved threads included (potentially larger response) + performance_monitor.start_timing("large_response_fetch") + + result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + str(pr_number), + "--resolved", # Include resolved threads + "--limit", + "100", # Request large limit + "--format", + "json", + ], + ) + + large_response_duration = performance_monitor.stop_timing() + + # Must succeed and stay within the time budget + assert result.exit_code == 0, f"fetch failed:\n{result.output}" + performance_monitor.assert_performance_threshold("large_response_fetch", 60.0) + + try: + threads = json.loads(result.output) + assert isinstance(threads, list) + + # Log the size for monitoring + print(f"Fetched {len(threads)} threads in {large_response_duration:.2f}s") + + except json.JSONDecodeError: + pytest.fail(f"Large response was not valid JSON: {result.output[:500]}...") + + def test_concurrent_api_usage_patterns( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + rate_limit_aware_delay, + skip_if_slow, + ): + """Test behavior under concurrent API usage.""" + pr_number = verify_test_pr_exists["number"] + + # Make several concurrent-like requests + results = [] + + for i in range(5): + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + results.append((i, result)) + + # Small delay to simulate concurrent usage + rate_limit_aware_delay(0.5) + + # Analyze results + successful_count = 0 + failed_count = 0 + + for i, result in results: + if result.exit_code == 0: + successful_count += 1 + try: + threads = json.loads(result.output) + assert isinstance(threads, list) + except json.JSONDecodeError: + pytest.fail(f"Request {i} returned invalid JSON: {result.output}") + else: + failed_count += 1 + # Check if failure is due to rate limiting + if "rate limit" in result.output.lower(): + print(f"Request {i} hit rate limit") + else: + print(f"Request {i} failed: {result.output}") + + print(f"Concurrent usage: {successful_count} success, {failed_count} failed") + + # At least some requests should succeed + assert successful_count > 0, "All concurrent requests failed" + + def test_progressive_backoff_behavior( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + rate_limit_aware_delay, + skip_if_slow, + ): + """Test progressive backoff when approaching rate limits.""" + pr_number = verify_test_pr_exists["number"] + + # Make rapid requests to potentially trigger rate limiting + backoff_timings = [] + + for i in range(10): + start_time = time.time() + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + end_time = time.time() + duration = end_time - start_time + backoff_timings.append(duration) + + if result.exit_code != 0: + if "rate limit" in result.output.lower(): + print(f"Hit rate limit on request {i+1}") + break + print(f"Request {i+1} failed for other reason: {result.output}") + + # Small delay between requests + rate_limit_aware_delay(0.2) + + print(f"Request timings: {[f'{t:.2f}s' for t in backoff_timings]}") + + # Add assertions to verify backoff behavior + assert backoff_timings, "No timings collected" + assert max(backoff_timings) <= 60, "Requests stalled >60s โ€“ backoff excessive" + + # If we hit rate limits, later requests should potentially take longer + # (This is implementation dependent and might not always be observable) + + def test_connection_recovery_after_failure( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + api_retry_helper, + ): + """Test recovery after connection failures.""" + pr_number = verify_test_pr_exists["number"] + + # First, make a successful request + first_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + if first_result.exit_code != 0: + pytest.skip("Initial connection failed, cannot test recovery") + + # Simulate some delay (as if recovering from failure) + time.sleep(2.0) + + # Make another request - should work if connection is stable + def recovery_fetch(): + return integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + recovery_result = api_retry_helper(recovery_fetch) + + # Should recover and work + if recovery_result.exit_code != 0: + print(f"Recovery failed: {recovery_result.output}") + + # Both requests should have similar behavior + assert first_result.exit_code == recovery_result.exit_code + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.resilience +class TestRateLimitHandling: + """Test GitHub API rate limit handling and compliance.""" + + def test_rate_limit_status_monitoring( + self, + integration_test_config: dict[str, Any], + ): + """Test monitoring of rate limit status.""" + # Skip if GH_TOKEN is not available (CI environment without auth) + if not os.getenv("GH_TOKEN") and not os.getenv("GITHUB_TOKEN"): + # Test if gh CLI is authenticated + auth_check = subprocess.run( + ["gh", "auth", "status"], capture_output=True, text=True, timeout=5 + ) + if auth_check.returncode != 0: + pytest.skip("GitHub CLI not authenticated and no GH_TOKEN available") + + try: + # Check initial rate limit status + initial_result = subprocess.run( + ["gh", "api", "rate_limit"], capture_output=True, text=True, timeout=10 + ) + + if initial_result.returncode != 0: + pytest.skip(f"Cannot access GitHub API: {initial_result.stderr}") + + assert initial_result.returncode == 0 + + initial_data = json.loads(initial_result.stdout) + initial_remaining = initial_data["rate"]["remaining"] + + # Make a test API call + test_result = subprocess.run( + ["gh", "api", "user"], capture_output=True, text=True, timeout=10 + ) + + if test_result.returncode == 0: + # Check rate limit again + after_result = subprocess.run( + ["gh", "api", "rate_limit"], + capture_output=True, + text=True, + timeout=10, + ) + + assert after_result.returncode == 0 + + after_data = json.loads(after_result.stdout) + after_remaining = after_data["rate"]["remaining"] + + # Should have decremented (or stayed same if using cached results) + assert after_remaining <= initial_remaining + + print(f"Rate limit: {initial_remaining} -> {after_remaining}") + + except Exception as e: + pytest.fail(f"Rate limit monitoring failed: {e}") + + def test_rate_limit_respect_in_operations( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + integration_test_config: dict[str, Any], + ): + """Test that operations respect rate limits.""" + pr_number = verify_test_pr_exists["number"] + + # Check current rate limit + rate_result = subprocess.run( + ["gh", "api", "rate_limit"], capture_output=True, text=True, timeout=10 + ) + + if rate_result.returncode == 0: + rate_data = json.loads(rate_result.stdout) + remaining = rate_data["rate"]["remaining"] + + # If we're too close to the limit, skip this test + if remaining < integration_test_config["rate_limit_buffer"]: + pytest.skip(f"Too close to rate limit: {remaining}") + + # Make a toady operation + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + # Should complete successfully within rate limits + if result.exit_code != 0 and "rate limit" in result.output.lower(): + pytest.fail( + "Operation failed due to rate limiting when sufficient " + "quota available" + ) + + def test_rate_limit_error_handling( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + ): + """Test graceful handling when rate limits are exceeded.""" + verify_test_pr_exists["number"] + + # This test is difficult to implement safely without actually + # exhausting rate limits, which would affect other tests + + # Instead, we test that our error handling would work correctly + # by checking the error message format for rate limit scenarios + + # We can check current rate limit and warn if we're close + try: + rate_result = subprocess.run( + ["gh", "api", "rate_limit"], capture_output=True, text=True, timeout=10 + ) + + if rate_result.returncode == 0: + rate_data = json.loads(rate_result.stdout) + remaining = rate_data["rate"]["remaining"] + limit = rate_data["rate"]["limit"] + reset_time = rate_data["rate"]["reset"] + + print( + f"Current rate limit: {remaining}/{limit} (resets at {reset_time})" + ) + + if remaining < 100: + print("WARNING: Low rate limit remaining") + + except Exception as e: + print(f"Could not check rate limit: {e}") + + def test_secondary_rate_limit_handling(self): + """Test handling of secondary rate limits (abuse detection).""" + # Secondary rate limits are harder to test consistently + # as they depend on GitHub's abuse detection algorithms + + # We can test that our code would handle such errors appropriately + # by ensuring our error handling covers the expected error codes + + # For now, this is a placeholder for future implementation + # when we have better ways to simulate these conditions + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.resilience +class TestErrorRecoveryPatterns: + """Test error recovery and resilience patterns.""" + + def test_transient_error_recovery( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + api_retry_helper, + ): + """Test recovery from transient errors.""" + pr_number = verify_test_pr_exists["number"] + + def potentially_failing_operation(): + return integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + # Use retry helper which implements backoff + result = api_retry_helper(potentially_failing_operation) + + # Should eventually succeed or give a consistent error + if result.exit_code != 0: + # Error should be informative, not a crash + assert len(result.output) > 0 + assert "traceback" not in result.output.lower() + + def test_partial_failure_handling( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + ): + """Test handling of partial failures in operations.""" + pr_number = verify_test_pr_exists["number"] + + # Test operations that might partially fail + # (e.g., some data available, some not) + + result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + str(pr_number), + "--limit", + "100", # Large limit that might hit boundaries + "--format", + "json", + ], + ) + + if result.exit_code == 0: + try: + threads = json.loads(result.output) + # Should get some data, even if not everything requested + assert isinstance(threads, list) + + except json.JSONDecodeError: + pytest.fail(f"Partial success returned invalid JSON: {result.output}") + else: + # If it fails, should be a clear error + assert "error" in result.output.lower() or "failed" in result.output.lower() + + def test_data_consistency_after_errors( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + rate_limit_aware_delay, + ): + """Test that data remains consistent after error conditions.""" + pr_number = verify_test_pr_exists["number"] + + # Get baseline data + baseline_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + if baseline_result.exit_code != 0: + pytest.skip("Cannot establish baseline data") + + baseline_threads = json.loads(baseline_result.output) + + # Wait a bit (simulate time passing) + rate_limit_aware_delay(3.0) + + # Get data again + consistency_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + if consistency_result.exit_code == 0: + consistency_threads = json.loads(consistency_result.output) + + # Basic consistency checks + baseline_ids = {t["thread_id"] for t in baseline_threads} + consistency_ids = {t["thread_id"] for t in consistency_threads} + + # Thread IDs should be stable (assuming no external changes) + # We allow for some differences due to potential external modifications + common_ids = baseline_ids & consistency_ids + + if baseline_ids and consistency_ids: + # Should have significant overlap unless there were major changes + overlap_ratio = len(common_ids) / max( + len(baseline_ids), len(consistency_ids) + ) + + if overlap_ratio < 0.8: # Allow 20% change due to external factors + print(f"Warning: Data consistency low: {overlap_ratio:.2%} overlap") + + def test_graceful_degradation_patterns( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + ): + """Test graceful degradation when services are limited.""" + pr_number = verify_test_pr_exists["number"] + + # Test with various constraint scenarios + constraint_tests = [ + (["fetch", "--pr", str(pr_number), "--limit", "1"], "small_limit"), + (["fetch", "--pr", str(pr_number), "--format", "json"], "standard"), + ] + + for cmd_args, test_name in constraint_tests: + result = integration_cli_runner.invoke(cli, cmd_args) + + # Should either work or fail gracefully + if result.exit_code == 0: + try: + if "--format" in cmd_args and "json" in cmd_args: + threads = json.loads(result.output) + assert isinstance(threads, list) + except json.JSONDecodeError: + pytest.fail(f"{test_name} returned invalid JSON: {result.output}") + else: + # Failure should be informative + assert len(result.output) > 0 + print(f"{test_name} failed gracefully: {result.output[:100]}...") diff --git a/tests/integration/real_api/test_performance_monitoring.py b/tests/integration/real_api/test_performance_monitoring.py new file mode 100644 index 0000000..fd0e153 --- /dev/null +++ b/tests/integration/real_api/test_performance_monitoring.py @@ -0,0 +1,610 @@ +"""Integration tests for performance monitoring and benchmarking. + +These tests measure and validate performance characteristics of real API +interactions, including response times, memory usage, and scalability. +""" + +import json +import time +from typing import Any + +from click.testing import CliRunner +import pytest + +from toady.cli import cli + +try: + import psutil +except ImportError: + psutil = None + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.performance +@pytest.mark.slow +class TestPerformanceBenchmarks: + """Test performance characteristics of API operations.""" + + def test_fetch_performance_baseline( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + ): + """Establish performance baseline for fetch operations.""" + pr_number = verify_test_pr_exists["number"] + + # Warm-up call (exclude from timing) + warmup_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + if warmup_result.exit_code != 0: + pytest.skip(f"Warmup fetch failed: {warmup_result.output}") + + # Measure multiple fetch operations + timings = [] + thread_counts = [] + + for i in range(5): + performance_monitor.start_timing(f"fetch_baseline_{i}") + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + duration = performance_monitor.stop_timing() + timings.append(duration) + + assert result.exit_code == 0, f"Fetch {i+1} failed: {result.output}" + + threads = json.loads(result.output) + thread_counts.append(len(threads)) + + # Small delay between calls + time.sleep(1.0) + + # Calculate statistics + avg_time = sum(timings) / len(timings) + max_time = max(timings) + min_time = min(timings) + avg_threads = sum(thread_counts) / len(thread_counts) + + print("Fetch performance baseline:") + print(f" Average time: {avg_time:.2f}s") + print(f" Min/Max time: {min_time:.2f}s / {max_time:.2f}s") + print(f" Average threads: {avg_threads:.1f}") + + # Performance assertions + assert avg_time < 10.0, f"Average fetch time too slow: {avg_time:.2f}s" + assert max_time < 20.0, f"Maximum fetch time too slow: {max_time:.2f}s" + + # Consistency check + time_variance = max_time - min_time + assert ( + time_variance < 15.0 + ), f"Timing too inconsistent: {time_variance:.2f}s variance" + + def test_memory_usage_patterns( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + skip_if_slow, + ): + """Monitor memory usage patterns during operations.""" + if psutil is None: + pytest.skip("psutil not available for memory monitoring") + + pr_number = verify_test_pr_exists["number"] + + # Get initial memory usage + process = psutil.Process() + initial_memory = process.memory_info().rss / 1024 / 1024 # MB + + memory_measurements = [initial_memory] + + # Perform multiple operations while monitoring memory + for i in range(10): + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + current_memory = process.memory_info().rss / 1024 / 1024 # MB + memory_measurements.append(current_memory) + + if result.exit_code != 0: + print(f"Operation {i+1} failed: {result.output}") + break + + time.sleep(0.5) + + # Analyze memory usage + max_memory = max(memory_measurements) + final_memory = memory_measurements[-1] + memory_growth = final_memory - initial_memory + + print("Memory usage analysis:") + print(f" Initial: {initial_memory:.1f} MB") + print(f" Maximum: {max_memory:.1f} MB") + print(f" Final: {final_memory:.1f} MB") + print(f" Growth: {memory_growth:.1f} MB") + + # Memory usage assertions + assert ( + max_memory < initial_memory + 500 + ), f"Memory usage too high: {max_memory:.1f} MB" + assert ( + memory_growth < 100 + ), f"Memory leak detected: {memory_growth:.1f} MB growth" + + def test_large_pr_performance( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + skip_if_slow, + ): + """Test performance with large PRs (high thread count).""" + pr_number = verify_test_pr_exists["number"] + + # Test with maximum limit to stress test + performance_monitor.start_timing("large_pr_fetch") + + result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + str(pr_number), + "--resolved", # Include resolved threads for larger dataset + "--limit", + "100", + "--format", + "json", + ], + ) + + duration = performance_monitor.stop_timing() + + if result.exit_code == 0: + threads = json.loads(result.output) + thread_count = len(threads) + + print("Large PR performance:") + print(f" Threads fetched: {thread_count}") + print(f" Time taken: {duration:.2f}s") + + if thread_count > 0: + print(f" Threads per second: {thread_count / duration:.1f}") + + # Performance assertions based on data size + if thread_count > 100: + # For large datasets, allow more time + performance_monitor.assert_performance_threshold( + "large_pr_fetch", 60.0 + ) + elif thread_count > 10: + # Medium datasets + performance_monitor.assert_performance_threshold( + "large_pr_fetch", 30.0 + ) + else: + # Small datasets should be fast + performance_monitor.assert_performance_threshold( + "large_pr_fetch", 15.0 + ) + else: + # If fetch failed, still check that it failed quickly + assert duration < 30.0, f"Failed fetch took too long: {duration:.2f}s" + + def test_concurrent_performance_impact( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + skip_if_slow, + ): + """Test performance impact of concurrent operations.""" + pr_number = verify_test_pr_exists["number"] + + # Single operation baseline + performance_monitor.start_timing("single_operation") + + single_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + single_duration = performance_monitor.stop_timing() + + if single_result.exit_code != 0: + pytest.skip(f"Single operation failed: {single_result.output}") + + # Multiple rapid operations + rapid_timings = [] + + for i in range(5): + performance_monitor.start_timing(f"rapid_operation_{i}") + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + timing = performance_monitor.stop_timing() + rapid_timings.append(timing) + + if result.exit_code != 0: + print(f"Rapid operation {i+1} failed: {result.output}") + + # Minimal delay to simulate rapid usage + time.sleep(0.2) + + # Analyze performance impact + avg_rapid_time = sum(rapid_timings) / len(rapid_timings) + slowdown_factor = ( + avg_rapid_time / single_duration if single_duration > 0 else 1.0 + ) + + print("Concurrent performance analysis:") + print(f" Single operation: {single_duration:.2f}s") + print(f" Average rapid operation: {avg_rapid_time:.2f}s") + print(f" Slowdown factor: {slowdown_factor:.2f}x") + + # Performance should not degrade excessively + assert ( + slowdown_factor < 3.0 + ), f"Excessive slowdown under load: {slowdown_factor:.2f}x" + + def test_cache_performance_impact( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + temp_cache_dir, + ): + """Test performance impact of caching mechanisms.""" + pr_number = verify_test_pr_exists["number"] + + # First call (cold cache) + performance_monitor.start_timing("cold_cache_fetch") + + first_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + cold_duration = performance_monitor.stop_timing() + + if first_result.exit_code != 0: + pytest.skip(f"Cold cache fetch failed: {first_result.output}") + + # Wait a moment + time.sleep(1.0) + + # Second call (potentially warm cache) + performance_monitor.start_timing("warm_cache_fetch") + + second_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + warm_duration = performance_monitor.stop_timing() + + if second_result.exit_code == 0: + # Compare results + first_threads = json.loads(first_result.output) + second_threads = json.loads(second_result.output) + + # Results should be consistent + assert len(first_threads) == len( + second_threads + ), "Cache returned different data" + + # Performance analysis + speedup = cold_duration / warm_duration if warm_duration > 0 else 1.0 + + print("Cache performance analysis:") + print(f" Cold cache: {cold_duration:.2f}s") + print(f" Warm cache: {warm_duration:.2f}s") + print(f" Speedup: {speedup:.2f}x") + + # Cache should not significantly slow things down + assert ( + warm_duration <= cold_duration * 2.0 + ), "Cache caused significant slowdown" + + def test_network_latency_impact( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + network_simulation, + ): + """Test performance under various network conditions.""" + pr_number = verify_test_pr_exists["number"] + + # Baseline measurement + performance_monitor.start_timing("baseline_fetch") + + baseline_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + baseline_duration = performance_monitor.stop_timing() + + if baseline_result.exit_code != 0: + pytest.skip(f"Baseline fetch failed: {baseline_result.output}") + + # Test with simulated slow connection + # (Note: This is limited simulation - real network testing would require + # network manipulation tools) + + network_simulation.simulate_slow_connection(1.0) + + performance_monitor.start_timing("slow_network_fetch") + + slow_result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + slow_duration = performance_monitor.stop_timing() + + if slow_result.exit_code == 0: + print("Network latency impact:") + print(f" Baseline: {baseline_duration:.2f}s") + print(f" With simulated delay: {slow_duration:.2f}s") + + # Results should still be consistent + baseline_threads = json.loads(baseline_result.output) + slow_threads = json.loads(slow_result.output) + + assert len(baseline_threads) == len( + slow_threads + ), "Network delay affected data consistency" + + +@pytest.mark.integration +@pytest.mark.real_api +@pytest.mark.performance +class TestScalabilityPatterns: + """Test scalability characteristics and patterns.""" + + def test_thread_count_scaling( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + ): + """Test performance scaling with different thread counts.""" + pr_number = verify_test_pr_exists["number"] + + # Test with different limits + limits = [1, 5, 10, 50, 100] + scaling_data = [] + + for limit in limits: + performance_monitor.start_timing(f"scaling_limit_{limit}") + + result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + str(pr_number), + "--limit", + str(limit), + "--format", + "json", + ], + ) + + duration = performance_monitor.stop_timing() + + if result.exit_code == 0: + threads = json.loads(result.output) + actual_count = len(threads) + + scaling_data.append( + { + "limit": limit, + "actual_count": actual_count, + "duration": duration, + "threads_per_second": ( + actual_count / duration if duration > 0 else 0 + ), + } + ) + + print(f"Limit {limit}: {actual_count} threads in {duration:.2f}s") + else: + print(f"Limit {limit} failed: {result.output}") + + time.sleep(1.0) # Rate limiting courtesy + + # Analyze scaling patterns + if len(scaling_data) >= 2: + # Check if performance scales reasonably + first = scaling_data[0] + last = scaling_data[-1] + + if last["actual_count"] > first["actual_count"]: + efficiency_ratio = ( + last["threads_per_second"] / first["threads_per_second"] + ) + + print(f"Scaling efficiency: {efficiency_ratio:.2f}") + + # Performance should not degrade dramatically with scale + assert ( + efficiency_ratio > 0.1 + ), f"Poor scaling efficiency: {efficiency_ratio:.2f}" + + def test_data_processing_efficiency( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + performance_monitor, + ): + """Test efficiency of data processing operations.""" + pr_number = verify_test_pr_exists["number"] + + # Fetch data for processing analysis + performance_monitor.start_timing("data_fetch_for_processing") + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + fetch_duration = performance_monitor.stop_timing() + + if result.exit_code != 0: + pytest.skip(f"Data fetch failed: {result.output}") + + threads = json.loads(result.output) + + if not threads: + pytest.skip("No threads available for processing analysis") + + # Analyze data structure complexity + total_comments = sum(len(thread.get("comments", [])) for thread in threads) + total_chars = sum( + len(comment.get("content", "")) + for thread in threads + for comment in thread.get("comments", []) + ) + + print("Data processing analysis:") + print(f" Threads: {len(threads)}") + print(f" Comments: {total_comments}") + print(f" Characters: {total_chars}") + print(f" Processing time: {fetch_duration:.2f}s") + + if total_chars > 0: + chars_per_second = total_chars / fetch_duration + print(f" Processing rate: {chars_per_second:.0f} chars/sec") + + # Reasonable processing efficiency + assert ( + chars_per_second > 1000 + ), f"Processing too slow: {chars_per_second:.0f} chars/sec" + + def test_resource_utilization_patterns( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + skip_if_slow, + ): + """Test CPU and memory utilization patterns.""" + if psutil is None: + pytest.skip("psutil not available for resource monitoring") + + pr_number = verify_test_pr_exists["number"] + + process = psutil.Process() + + # Baseline measurements + initial_cpu_percent = process.cpu_percent() + initial_memory = process.memory_info().rss / 1024 / 1024 # MB + + # Perform resource-intensive operation + start_time = time.time() + + result = integration_cli_runner.invoke( + cli, + [ + "fetch", + "--pr", + str(pr_number), + "--resolved", + "--limit", + "100", + "--format", + "json", + ], + ) + + operation_duration = time.time() - start_time + + # Measure resource usage during operation + peak_cpu_percent = process.cpu_percent() + peak_memory = process.memory_info().rss / 1024 / 1024 # MB + + print("Resource utilization analysis:") + print(f" Operation duration: {operation_duration:.2f}s") + print(f" CPU usage: {initial_cpu_percent:.1f}% -> {peak_cpu_percent:.1f}%") + print(f" Memory usage: {initial_memory:.1f} MB -> {peak_memory:.1f} MB") + + if result.exit_code == 0: + threads = json.loads(result.output) + print(f" Threads processed: {len(threads)}") + + # Resource efficiency checks + memory_increase = peak_memory - initial_memory + + if len(threads) > 0: + memory_per_thread = memory_increase / len(threads) + print(f" Memory per thread: {memory_per_thread:.2f} MB") + + # Memory usage should be reasonable + assert ( + memory_per_thread < 10 + ), f"Excessive memory per thread: {memory_per_thread:.2f} MB" + + # CPU usage should be reasonable (not constantly maxed out) + # Note: This is environment-dependent + if peak_cpu_percent > 90: + print("Warning: High CPU usage detected") + + def test_long_running_stability( + self, + integration_cli_runner: CliRunner, + verify_test_pr_exists: dict[str, Any], + skip_if_slow, + ): + """Test stability over extended operation periods.""" + pr_number = verify_test_pr_exists["number"] + + # Run multiple operations over time to test stability + operation_count = 20 + success_count = 0 + failure_count = 0 + timings = [] + + for i in range(operation_count): + start_time = time.time() + + result = integration_cli_runner.invoke( + cli, ["fetch", "--pr", str(pr_number), "--format", "json"] + ) + + duration = time.time() - start_time + timings.append(duration) + + if result.exit_code == 0: + success_count += 1 + try: + threads = json.loads(result.output) + assert isinstance(threads, list) + except json.JSONDecodeError: + failure_count += 1 + print(f"Operation {i+1}: JSON decode failed") + else: + failure_count += 1 + print(f"Operation {i+1}: Command failed") + + # Delay between operations + time.sleep(2.0) + + # Analyze stability + success_rate = success_count / operation_count + avg_time = sum(timings) / len(timings) + time_variance = max(timings) - min(timings) + + print("Long-running stability analysis:") + print(f" Operations: {operation_count}") + print(f" Success rate: {success_rate:.1%}") + print(f" Average time: {avg_time:.2f}s") + print(f" Time variance: {time_variance:.2f}s") + + # Stability assertions + assert success_rate >= 0.8, f"Poor success rate: {success_rate:.1%}" + assert time_variance < 30.0, f"Excessive timing variance: {time_variance:.2f}s" diff --git a/tests/integration/test_schema_validation_integration.py b/tests/integration/test_schema_validation_integration.py index 132d25a..204c2d3 100644 --- a/tests/integration/test_schema_validation_integration.py +++ b/tests/integration/test_schema_validation_integration.py @@ -4,9 +4,9 @@ authentication via the gh CLI. """ +from pathlib import Path import subprocess import tempfile -from pathlib import Path import pytest diff --git a/tests/test_setup.py b/tests/test_setup.py index f941888..36f7f56 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -29,7 +29,6 @@ def test_package_files_exist(self) -> None: assert (root / "LICENSE").exists() assert (root / ".gitignore").exists() assert (root / "requirements.txt").exists() - assert (root / "requirements-dev.txt").exists() assert (root / "Makefile").exists() assert (root / "CHANGELOG.md").exists() diff --git a/tests/test_validation.py b/tests/test_validation.py index ea1d01e..2511978 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -7,7 +7,6 @@ from toady.exceptions import ValidationError from toady.validators.validation import ( ResolveOptions, - check_reply_content_warnings, validate_boolean_flag, validate_choice, validate_comment_id, @@ -20,6 +19,7 @@ validate_pr_number, validate_reply_body, validate_reply_command_args, + validate_reply_content_warnings, validate_resolve_command_args, validate_thread_id, validate_url, @@ -586,36 +586,36 @@ class TestCheckReplyContentWarnings: def test_no_warnings(self): """Test content with no warnings.""" - warnings = check_reply_content_warnings("This is a normal reply") + warnings = validate_reply_content_warnings("This is a normal reply") assert warnings == [] def test_mention_warning(self): """Test mention warning.""" - warnings = check_reply_content_warnings("@user this is a mention") + warnings = validate_reply_content_warnings("@user this is a mention") assert len(warnings) == 1 assert "mention users" in warnings[0] def test_repetitive_content_warning(self): """Test repetitive content warning.""" - warnings = check_reply_content_warnings("aaaaaaaaaaaaaaaa") + warnings = validate_reply_content_warnings("aaaaaaaaaaaaaaaa") assert len(warnings) == 1 assert "repetitive content" in warnings[0] def test_all_caps_warning(self): """Test all caps warning.""" - warnings = check_reply_content_warnings("THIS IS ALL CAPS CONTENT") + warnings = validate_reply_content_warnings("THIS IS ALL CAPS CONTENT") assert len(warnings) == 1 assert "ALL CAPS" in warnings[0] def test_excessive_punctuation_warning(self): """Test excessive punctuation warning.""" - warnings = check_reply_content_warnings("What?!?!?! Why?!?!?!") + warnings = validate_reply_content_warnings("What?!?!?! Why?!?!?!") assert len(warnings) == 1 assert "excessive punctuation" in warnings[0] def test_multiple_warnings(self): """Test content with multiple warnings.""" - warnings = check_reply_content_warnings("@USER WHAT?!?!?! WHY?!?!?!") + warnings = validate_reply_content_warnings("@USER WHAT?!?!?! WHY?!?!?!") assert len(warnings) == 3 # mention, all caps, excessive punctuation diff --git a/tests/unit/commands/README.md b/tests/unit/commands/README.md new file mode 100644 index 0000000..6e64ff6 --- /dev/null +++ b/tests/unit/commands/README.md @@ -0,0 +1,99 @@ +# Command Unit Tests + +This directory contains unit tests for command modules, focusing on testing the core command logic without testing the CLI interface directly. + +## Structure + +- `__init__.py` - Module initialization +- `test_fetch.py` - Comprehensive unit tests for the fetch command + +## Test Coverage + +### test_fetch.py + +Comprehensive unit tests for the fetch command (`src/toady/commands/fetch.py`) with 95.71% coverage including: + +#### Test Classes: + +1. **TestFetchCommandCore** - Basic command structure tests + - Command existence and definition + - Parameter configuration + - Default values + +2. **TestFetchCommandValidation** - Parameter validation tests + - PR number validation (called/not called scenarios) + - Limit validation + - Error handling for invalid inputs + +3. **TestFetchCommandFormatResolution** - Format option handling + - Pretty flag resolution + - Format parameter resolution + - Combined parameter scenarios + +4. **TestFetchCommandServiceIntegration** - Service layer integration + - FetchService instantiation with correct format + - Service method calls with correct parameters + - Interactive PR selection scenarios + +5. **TestFetchCommandThreadTypeDescription** - Thread type logic + - Unresolved threads description + - All threads description (with --resolved flag) + +6. **TestFetchCommandOutputFormatting** - Output formatting tests + - format_threads_output function calls + - Parameter passing to formatting functions + - Interactive vs explicit PR scenarios + +7. **TestFetchCommandExitConditions** - Exit behavior tests + - Clean exit on cancelled PR selection + - Normal completion on successful operations + +8. **TestFetchCommandErrorHandling** - Comprehensive error handling + - Click Exit exception re-raising + - Pretty format error messages + - JSON format error responses + - All exception types (Auth, Timeout, Rate Limit, Service, API, Unexpected) + +9. **TestFetchCommandParameterCombinations** - Parameter interaction tests + - All parameters specified together + - Minimal parameter usage + - Interactive selection with options + +10. **TestFetchCommandBoundaryConditions** - Edge case testing + - Maximum/minimum limit values + - Empty and large result sets + - Boundary value testing + +11. **TestFetchCommandMockingPatterns** - Test infrastructure validation + - Service mock patterns + - Format resolution mocking + - Click context usage + +## Test Patterns + +### Mocking Strategy +- Uses `@patch` decorators to mock dependencies +- Mocks `FetchService`, `resolve_format_from_options`, and `format_threads_output` +- Uses CLI runner approach via `runner.invoke(cli, ["fetch", ...])` for realistic testing + +### Fixture Usage +- Leverages existing `runner` fixture from conftest.py +- Uses module-scoped fixtures for performance +- Follows established patterns in the codebase + +### Coverage Goals +- Achieves 95.71% coverage on the fetch command module +- Tests all major code paths and error conditions +- Focuses on unit testing without integration dependencies + +## Usage + +Run the fetch command tests: +```bash +pytest tests/unit/commands/test_fetch.py -v +``` + +Run with coverage: +```bash +pytest tests/unit/commands/test_fetch.py --cov=src/toady/commands/fetch --cov-report=term-missing +``` diff --git a/tests/unit/commands/__init__.py b/tests/unit/commands/__init__.py new file mode 100644 index 0000000..dc08e26 --- /dev/null +++ b/tests/unit/commands/__init__.py @@ -0,0 +1 @@ +"""Unit tests for command modules.""" diff --git a/tests/unit/commands/test_fetch.py b/tests/unit/commands/test_fetch.py new file mode 100644 index 0000000..e112436 --- /dev/null +++ b/tests/unit/commands/test_fetch.py @@ -0,0 +1,733 @@ +"""Unit tests for the fetch command module. + +This module tests the core fetch command logic, including parameter validation, +error handling, format resolution, and service integration. It focuses on unit +testing the command implementation without testing the CLI interface directly. +""" + +import json +from unittest.mock import Mock, patch + +import click +from click.testing import CliRunner +import pytest + +from toady.cli import cli +from toady.commands.fetch import fetch +from toady.exceptions import ( + FetchServiceError, + GitHubAPIError, + GitHubAuthenticationError, + GitHubRateLimitError, + GitHubTimeoutError, +) + + +class TestFetchCommandCore: + """Test the core fetch command functionality.""" + + def test_fetch_command_exists(self): + """Test that the fetch command is properly defined.""" + assert fetch is not None + assert callable(fetch) + assert hasattr(fetch, "params") + + def test_fetch_command_parameters(self): + """Test that fetch command has expected parameters.""" + param_names = [param.name for param in fetch.params] + expected_params = ["pr_number", "format", "pretty", "resolved", "limit"] + + for expected_param in expected_params: + assert expected_param in param_names, f"Missing parameter: {expected_param}" + + def test_fetch_command_defaults(self): + """Test fetch command parameter defaults.""" + param_defaults = {param.name: param.default for param in fetch.params} + + assert param_defaults["limit"] == 100 + assert param_defaults["resolved"] is False + assert param_defaults["pretty"] is False + assert param_defaults["pr_number"] is None + assert param_defaults["format"] is None + + +class TestFetchCommandValidation: + """Test parameter validation in the fetch command.""" + + @patch("toady.commands.fetch.validate_pr_number") + @patch("toady.commands.fetch.validate_limit") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.FetchService") + def test_validation_called_with_pr_number( + self, + mock_service_class, + mock_resolve_format, + mock_validate_limit, + mock_validate_pr, + runner, + ): + """Test that validation is called when PR number is provided.""" + # Setup mocks + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + # Invoke command via CLI runner + runner.invoke(cli, ["fetch", "--pr", "123"]) + + # Verify validation was called + mock_validate_pr.assert_called_once_with(123) + mock_validate_limit.assert_called_once_with(100, max_limit=1000) + + @patch("toady.commands.fetch.validate_pr_number") + @patch("toady.commands.fetch.validate_limit") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.FetchService") + def test_validation_not_called_without_pr_number( + self, + mock_service_class, + mock_resolve_format, + mock_validate_limit, + mock_validate_pr, + runner, + ): + """Test that PR validation is not called when PR number is None.""" + # Setup mocks + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], None) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + # Invoke command via CLI runner + runner.invoke(cli, ["fetch"]) + + # Verify PR validation was not called, but limit validation was + mock_validate_pr.assert_not_called() + mock_validate_limit.assert_called_once_with(100, max_limit=1000) + + def test_validation_error_exits_with_error_code(self, runner): + """Test that validation errors cause exit with error code.""" + # Test with invalid PR number that will trigger validation error + result = runner.invoke(cli, ["fetch", "--pr", "-1"]) + assert result.exit_code != 0 + assert "PR number must be positive" in result.output + + def test_limit_validation_error_exits_with_error_code(self, runner): + """Test that limit validation errors cause exit with error code.""" + # Test with invalid limit that will trigger validation error + result = runner.invoke(cli, ["fetch", "--pr", "123", "--limit", "1001"]) + assert result.exit_code != 0 + assert "Limit cannot exceed 1000" in result.output + + +class TestFetchCommandFormatResolution: + """Test format resolution logic in the fetch command.""" + + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.FetchService") + def test_format_resolution_with_pretty_flag( + self, mock_service_class, mock_resolve_format, runner + ): + """Test format resolution with pretty flag.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + runner.invoke(cli, ["fetch", "--pr", "123", "--pretty"]) + + mock_resolve_format.assert_called_once_with(None, True) + + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.FetchService") + def test_format_resolution_with_format_parameter( + self, mock_service_class, mock_resolve_format, runner + ): + """Test format resolution with explicit format parameter.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--pr", "123", "--format", "json"]) + + mock_resolve_format.assert_called_once_with("json", False) + + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.FetchService") + def test_format_resolution_both_parameters( + self, mock_service_class, mock_resolve_format, runner + ): + """Test format resolution with both format and pretty parameters.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + runner.invoke(cli, ["fetch", "--pr", "123", "--format", "pretty", "--pretty"]) + + mock_resolve_format.assert_called_once_with("pretty", True) + + +class TestFetchCommandServiceIntegration: + """Test integration with FetchService in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_fetch_service_created_with_format( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that FetchService is created with resolved format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + runner.invoke(cli, ["fetch", "--pr", "123", "--pretty"]) + + mock_service_class.assert_called_once_with(output_format="pretty") + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_fetch_service_called_with_correct_parameters( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that FetchService is called with correct parameters.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 456) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--pr", "456", "--resolved", "--limit", "50"]) + + mock_service.fetch_review_threads_with_pr_selection.assert_called_once_with( + pr_number=456, include_resolved=True, threads_limit=50 + ) + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_fetch_service_called_for_interactive_selection( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that FetchService is called correctly for interactive PR selection.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], None) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch"]) + + mock_service.fetch_review_threads_with_pr_selection.assert_called_once_with( + pr_number=None, include_resolved=False, threads_limit=100 + ) + + +class TestFetchCommandThreadTypeDescription: + """Test thread type description generation in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.format_threads_output") + def test_unresolved_threads_description( + self, mock_format_output, mock_resolve_format, mock_service_class, runner + ): + """Test that unresolved threads description is correct.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--pr", "123"]) + + mock_format_output.assert_called_once() + call_args = mock_format_output.call_args[1] # Get keyword arguments + assert call_args["thread_type"] == "unresolved threads" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.format_threads_output") + def test_all_threads_description( + self, mock_format_output, mock_resolve_format, mock_service_class, runner + ): + """Test that all threads description is correct when resolved=True.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--pr", "123", "--resolved"]) + + mock_format_output.assert_called_once() + call_args = mock_format_output.call_args[1] # Get keyword arguments + assert call_args["thread_type"] == "all threads" + + +class TestFetchCommandOutputFormatting: + """Test output formatting in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.format_threads_output") + def test_format_threads_output_called_correctly( + self, mock_format_output, mock_resolve_format, mock_service_class, runner + ): + """Test that format_threads_output is called with correct parameters.""" + threads = [Mock()] + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ( + threads, + 123, + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + runner.invoke( + cli, ["fetch", "--pr", "123", "--pretty", "--resolved", "--limit", "50"] + ) + + mock_format_output.assert_called_once_with( + threads=threads, + format_name="pretty", + show_progress=True, + pr_number=123, + thread_type="all threads", + limit=50, + ) + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.format_threads_output") + def test_format_threads_output_with_interactive_selection( + self, mock_format_output, mock_resolve_format, mock_service_class, runner + ): + """Test format_threads_output with interactive PR selection.""" + threads = [Mock()] + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ( + threads, + 789, + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch"]) + + mock_format_output.assert_called_once_with( + threads=threads, + format_name="json", + show_progress=True, + pr_number=789, + thread_type="unresolved threads", + limit=100, + ) + + +class TestFetchCommandExitConditions: + """Test exit conditions in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_exit_on_cancelled_pr_selection( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that command exits cleanly when PR selection is cancelled.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], None) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch"]) + assert result.exit_code == 0 + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_no_exit_with_successful_selection( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that command doesn't exit when PR selection is successful.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "123"]) + assert result.exit_code == 0 + + +class TestFetchCommandErrorHandling: + """Test error handling in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_click_exit_exception_reraised( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that Click Exit exceptions are re-raised.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + click.exceptions.Exit(5) + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "123"]) + assert result.exit_code == 5 + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_pretty_format_error_handling( + self, mock_resolve_format, mock_service_class, runner + ): + """Test error handling in pretty format mode.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubAuthenticationError("Auth failed") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + result = runner.invoke(cli, ["fetch", "--pr", "123", "--pretty"]) + + # Should exit with authentication error code + assert result.exit_code == 41 + assert "GitHub authentication failed" in result.output + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_authentication_error( + self, mock_resolve_format, mock_service_class, runner + ): + """Test authentication error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubAuthenticationError("Auth failed") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "123"]) + assert result.exit_code == 1 + + # Should output JSON error + output = json.loads(result.output) + assert output["success"] is False + assert output["error"] == "authentication_failed" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_timeout_error( + self, mock_resolve_format, mock_service_class, runner + ): + """Test timeout error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubTimeoutError("Timeout") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "456"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "timeout" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_rate_limit_error( + self, mock_resolve_format, mock_service_class, runner + ): + """Test rate limit error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubRateLimitError("Rate limit exceeded") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "789"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "rate_limit_exceeded" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_fetch_service_error( + self, mock_resolve_format, mock_service_class, runner + ): + """Test fetch service error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + FetchServiceError("Service error") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "111"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "service_error" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_github_api_error_404( + self, mock_resolve_format, mock_service_class, runner + ): + """Test GitHub API 404 error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubAPIError("404 Not Found") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "222"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "pr_not_found" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_github_api_error_403( + self, mock_resolve_format, mock_service_class, runner + ): + """Test GitHub API 403 error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubAPIError("403 Forbidden") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "333"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "permission_denied" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_github_api_error_general( + self, mock_resolve_format, mock_service_class, runner + ): + """Test general GitHub API error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ( + GitHubAPIError("500 Internal Server Error") + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "444"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "api_error" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_json_format_unexpected_error( + self, mock_resolve_format, mock_service_class, runner + ): + """Test unexpected error handling in JSON format.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.side_effect = ValueError( + "Unexpected error" + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "555"]) + assert result.exit_code == 1 + + output = json.loads(result.output) + assert output["error"] == "internal_error" + + +class TestFetchCommandParameterCombinations: + """Test various parameter combinations in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_all_parameters_combination( + self, mock_resolve_format, mock_service_class, runner + ): + """Test fetch command with all parameters specified.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 999) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + runner.invoke( + cli, + [ + "fetch", + "--pr", + "999", + "--format", + "pretty", + "--pretty", + "--resolved", + "--limit", + "500", + ], + ) + + # Verify service was called with correct parameters + mock_service.fetch_review_threads_with_pr_selection.assert_called_once_with( + pr_number=999, include_resolved=True, threads_limit=500 + ) + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_minimal_parameters_combination( + self, mock_resolve_format, mock_service_class, runner + ): + """Test fetch command with minimal parameters (defaults).""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], None) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch"]) + + # Should exit cleanly when no PR is selected + assert result.exit_code == 0 + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_interactive_with_resolved_and_custom_limit( + self, mock_resolve_format, mock_service_class, runner + ): + """Test interactive PR selection with resolved threads and custom limit.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 777) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--resolved", "--limit", "25"]) + + mock_service.fetch_review_threads_with_pr_selection.assert_called_once_with( + pr_number=None, include_resolved=True, threads_limit=25 + ) + + +class TestFetchCommandBoundaryConditions: + """Test boundary conditions and edge cases in the fetch command.""" + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_maximum_limit_value(self, mock_resolve_format, mock_service_class, runner): + """Test fetch command with maximum allowed limit.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--pr", "123", "--limit", "1000"]) + + mock_service.fetch_review_threads_with_pr_selection.assert_called_once_with( + pr_number=123, include_resolved=False, threads_limit=1000 + ) + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_minimum_limit_value(self, mock_resolve_format, mock_service_class, runner): + """Test fetch command with minimum allowed limit.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["fetch", "--pr", "123", "--limit", "1"]) + + mock_service.fetch_review_threads_with_pr_selection.assert_called_once_with( + pr_number=123, include_resolved=False, threads_limit=1 + ) + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + def test_empty_threads_result( + self, mock_resolve_format, mock_service_class, runner + ): + """Test fetch command when no threads are returned.""" + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "123"]) + + # Should complete normally with empty result + assert result.exit_code == 0 + + @patch("toady.commands.fetch.FetchService") + @patch("toady.commands.fetch.resolve_format_from_options") + @patch("toady.commands.fetch.format_threads_output") + def test_large_threads_result( + self, mock_format_output, mock_resolve_format, mock_service_class, runner + ): + """Test fetch command with large number of threads.""" + # Create mock threads + mock_threads = [Mock() for _ in range(100)] + + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ( + mock_threads, + 123, + ) + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke(cli, ["fetch", "--pr", "123"]) + + # Should complete normally with large result + assert result.exit_code == 0 + + +class TestFetchCommandMockingPatterns: + """Test that mocking patterns follow established conventions.""" + + @patch("toady.commands.fetch.FetchService") + def test_service_mock_spec_usage(self, mock_service_class, runner): + """Test that service mocks follow proper spec patterns.""" + # Verify mock is configured properly + assert mock_service_class.called is False + + # Create mock service instance + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ([], 123) + mock_service_class.return_value = mock_service + + # Verify mock configuration + assert mock_service_class.return_value == mock_service + assert hasattr(mock_service, "fetch_review_threads_with_pr_selection") + + @patch("toady.commands.fetch.resolve_format_from_options") + def test_format_resolution_mock_usage(self, mock_resolve_format, runner): + """Test format resolution mocking patterns.""" + mock_resolve_format.return_value = "json" + + # Verify mock is properly configured + assert mock_resolve_format.return_value == "json" + assert callable(mock_resolve_format) + + def test_click_context_usage(self, runner): + """Test that Click context is used properly in tests.""" + # Test runner usage + assert runner is not None + assert hasattr(runner, "invoke") + + # Test command definition + assert fetch is not None + assert hasattr(fetch, "name") + + +@pytest.fixture(scope="module") +def runner(): + """Create a Click CLI test runner for the module.""" + return CliRunner() diff --git a/tests/unit/commands/test_reply.py b/tests/unit/commands/test_reply.py new file mode 100644 index 0000000..122c397 --- /dev/null +++ b/tests/unit/commands/test_reply.py @@ -0,0 +1,929 @@ +"""Unit tests for the reply command module. + +This module tests the core reply command logic, including parameter validation, +error handling, format resolution, and service integration. It focuses on unit +testing the command implementation without testing the CLI interface directly. +""" + +import json +from unittest.mock import Mock, patch + +import click +from click.testing import CliRunner +import pytest + +from toady.cli import cli +from toady.commands.reply import ( + _build_json_reply, + _handle_reply_error, + _print_pretty_reply, + _show_id_help, + _show_progress, + _show_warnings, + _validate_reply_args, + reply, + validate_reply_target_id, +) +from toady.exceptions import ( + GitHubAPIError, + GitHubAuthenticationError, + GitHubRateLimitError, + GitHubTimeoutError, +) +from toady.services.reply_service import ( + CommentNotFoundError, + ReplyRequest, + ReplyServiceError, +) + + +class TestReplyCommandCore: + """Test the core reply command functionality.""" + + def test_reply_command_exists(self): + """Test that the reply command is properly defined.""" + assert reply is not None + assert callable(reply) + assert hasattr(reply, "params") + + def test_reply_command_parameters(self): + """Test that reply command has expected parameters.""" + param_names = [param.name for param in reply.params] + expected_params = ["id", "body", "format", "pretty", "verbose", "help_ids"] + + for expected_param in expected_params: + assert expected_param in param_names, f"Missing parameter: {expected_param}" + + def test_reply_command_defaults(self): + """Test reply command parameter defaults.""" + param_defaults = {param.name: param.default for param in reply.params} + + assert param_defaults["id"] is None + assert param_defaults["body"] is None + assert param_defaults["format"] is None + assert param_defaults["pretty"] is False + assert param_defaults["verbose"] is False + assert param_defaults["help_ids"] is False + + +class TestValidateReplyTargetId: + """Test the validate_reply_target_id function.""" + + @patch("toady.commands.reply.create_universal_validator") + def test_valid_numeric_id(self, mock_create_validator): + """Test validation of valid numeric ID.""" + mock_validator = Mock() + mock_validator.validate_id.return_value = Mock(value="numeric") + mock_create_validator.return_value = mock_validator + + result = validate_reply_target_id("123456789") + assert result == "123456789" + mock_validator.validate_id.assert_called_once_with( + "123456789", "Reply target ID" + ) + + @patch("toady.commands.reply.create_universal_validator") + def test_valid_node_id(self, mock_create_validator): + """Test validation of valid node ID.""" + mock_validator = Mock() + mock_validator.validate_id.return_value = Mock(value="IC_") + mock_create_validator.return_value = mock_validator + + result = validate_reply_target_id("IC_kwDOABcD12MAAAABcDE3fg") + assert result == "IC_kwDOABcD12MAAAABcDE3fg" + + @patch("toady.commands.reply.create_universal_validator") + def test_valid_thread_id(self, mock_create_validator): + """Test validation of valid thread ID.""" + mock_validator = Mock() + mock_validator.validate_id.return_value = Mock(value="RT_") + mock_create_validator.return_value = mock_validator + + result = validate_reply_target_id("RT_kwDOABcD12MAAAABcDE3fg") + assert result == "RT_kwDOABcD12MAAAABcDE3fg" + + def test_empty_id_error(self): + """Test validation error for empty ID.""" + with pytest.raises(click.BadParameter) as exc_info: + validate_reply_target_id("") + assert "Reply target ID cannot be empty" in str(exc_info.value) + + def test_whitespace_id_error(self): + """Test validation error for whitespace-only ID.""" + with pytest.raises(click.BadParameter) as exc_info: + validate_reply_target_id(" ") + assert "Reply target ID cannot be empty" in str(exc_info.value) + + @patch("toady.commands.reply.create_universal_validator") + def test_prrc_id_error(self, mock_create_validator): + """Test special error handling for PRRC_ IDs.""" + mock_validator = Mock() + mock_validator.validate_id.return_value = Mock(value="PRRC_") + mock_create_validator.return_value = mock_validator + + with pytest.raises(click.BadParameter) as exc_info: + validate_reply_target_id("PRRC_kwDOABcD12MAAAABcDE3fg") + + error_msg = str(exc_info.value) + assert "Individual comment IDs from submitted reviews (PRRC_)" in error_msg + assert "Use the thread ID instead" in error_msg + assert "toady reply --help-ids" in error_msg + + @patch("toady.commands.reply.create_universal_validator") + def test_invalid_format_error_enhancement(self, mock_create_validator): + """Test error message enhancement for invalid format.""" + mock_validator = Mock() + mock_validator.validate_id.side_effect = ValueError("must start with one of") + mock_create_validator.return_value = mock_validator + + with pytest.raises(click.BadParameter) as exc_info: + validate_reply_target_id("invalid123") + + error_msg = str(exc_info.value) + assert "must start with one of" in error_msg + assert "Common ID types for replies" in error_msg + assert "Thread IDs: PRRT_, PRT_, RT_" in error_msg + assert "toady reply --help-ids" in error_msg + + @patch("toady.commands.reply.create_universal_validator") + def test_other_validation_error_enhancement(self, mock_create_validator): + """Test error message enhancement for other validation errors.""" + mock_validator = Mock() + mock_validator.validate_id.side_effect = ValueError("appears too short") + mock_create_validator.return_value = mock_validator + + with pytest.raises(click.BadParameter) as exc_info: + validate_reply_target_id("IC_abc") + + error_msg = str(exc_info.value) + assert "appears too short" in error_msg + assert "toady reply --help-ids" in error_msg + + +class TestValidateReplyArgs: + """Test the _validate_reply_args function.""" + + @patch("toady.commands.reply.validate_reply_target_id") + def test_valid_args(self, mock_validate_id): + """Test validation of valid arguments.""" + mock_validate_id.return_value = "123456789" + + reply_id, body = _validate_reply_args("123456789", "Valid reply message") + + assert reply_id == "123456789" + assert body == "Valid reply message" + mock_validate_id.assert_called_once_with("123456789") + + @patch("toady.commands.reply.validate_reply_target_id") + def test_body_whitespace_trimming(self, mock_validate_id): + """Test that body whitespace is trimmed.""" + mock_validate_id.return_value = "123456789" + + reply_id, body = _validate_reply_args("123456789", " Valid reply message ") + + assert body == "Valid reply message" + + def test_empty_body_error(self): + """Test validation error for empty body.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", "") + assert "Reply body cannot be empty" in str(exc_info.value) + + def test_whitespace_only_body_error(self): + """Test validation error for whitespace-only body.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", " ") + assert "Reply body cannot be empty" in str(exc_info.value) + + def test_body_too_long_error(self): + """Test validation error for body exceeding maximum length.""" + long_body = "x" * 65537 + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", long_body) + assert "Reply body cannot exceed 65,536 characters" in str(exc_info.value) + + def test_body_at_maximum_length(self): + """Test validation of body at maximum length.""" + max_body = "x" * 65536 + with patch("toady.commands.reply.validate_reply_target_id") as mock_validate_id: + mock_validate_id.return_value = "123456789" + reply_id, body = _validate_reply_args("123456789", max_body) + assert len(body) == 65536 + + def test_body_too_short_error(self): + """Test validation error for body too short.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", "ab") + assert "Reply body must be at least 3 characters long" in str(exc_info.value) + + def test_short_placeholder_text_error(self): + """Test validation error for short placeholder text. + + Hits length check first. + """ + # These hit the length check before the placeholder check + short_placeholders = [".", "..", "!!", "!?"] + + for placeholder in short_placeholders: + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", placeholder) + assert "Reply body must be at least 3 characters long" in str( + exc_info.value + ) + + def test_placeholder_text_error(self): + """Test validation error for placeholder text.""" + # Only test placeholder texts that are 3+ characters + # (length check comes first) + # From the actual implementation: + # [".", "..", "...", "????", "???", "!!", "!?", "???"] + placeholder_texts = ["...", "????", "???"] # The 3+ character ones + + for placeholder in placeholder_texts: + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", placeholder) + assert "Reply body appears to be placeholder text" in str(exc_info.value) + + def test_insufficient_non_whitespace_error(self): + """Test validation error for insufficient non-whitespace characters.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_reply_args("123456789", "a \n\t b") + assert "Reply body must contain at least 3 non-whitespace characters" in str( + exc_info.value + ) + + +class TestPrintPrettyReply: + """Test the _print_pretty_reply function.""" + + def test_basic_reply_info(self, capsys): + """Test printing basic reply information.""" + reply_info = { + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321", + "reply_id": "987654321", + } + + _print_pretty_reply(reply_info, verbose=False) + + captured = capsys.readouterr() + assert "โœ… Reply posted successfully" in captured.out + assert "๐Ÿ”— View reply at: https://github.com/owner/repo/pull/1" in captured.out + assert "๐Ÿ“ Reply ID: 987654321" in captured.out + + def test_reply_url_fragment_stripping(self, capsys): + """Test that URL fragments are stripped for display.""" + reply_info = { + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321" + } + + _print_pretty_reply(reply_info, verbose=False) + + captured = capsys.readouterr() + assert "๐Ÿ”— View reply at: https://github.com/owner/repo/pull/1" in captured.out + assert "#discussion_r" not in captured.out + + def test_verbose_reply_info(self, capsys): + """Test printing verbose reply information.""" + reply_info = { + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321", + "reply_id": "987654321", + "pr_number": "42", + "pr_title": "Add awesome feature", + "parent_comment_author": "reviewer123", + "body_preview": "This is a test reply...", + "thread_url": "https://github.com/owner/repo/pull/42#pullrequestreview-123456", + "created_at": "2023-01-01T12:00:00Z", + "author": "testuser", + } + + _print_pretty_reply(reply_info, verbose=True) + + captured = capsys.readouterr() + assert "๐Ÿ“‹ Reply Details:" in captured.out + assert "Pull Request: #42 - Add awesome feature" in captured.out + assert "Replying to: @reviewer123" in captured.out + assert "Your reply: This is a test reply..." in captured.out + assert ( + "Thread URL: https://github.com/owner/repo/pull/42#pullrequestreview-123456" + in captured.out + ) + assert "Posted at: 2023-01-01T12:00:00Z" in captured.out + assert "Posted by: @testuser" in captured.out + + def test_minimal_reply_info(self, capsys): + """Test printing with minimal reply information.""" + reply_info = {} + + _print_pretty_reply(reply_info, verbose=False) + + captured = capsys.readouterr() + assert "โœ… Reply posted successfully" in captured.out + # Should not show empty fields + assert "๐Ÿ”— View reply at:" not in captured.out + assert "๐Ÿ“ Reply ID:" not in captured.out + + +class TestBuildJsonReply: + """Test the _build_json_reply function.""" + + def test_basic_json_reply(self): + """Test building basic JSON reply.""" + reply_info = { + "reply_id": "987654321", + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321", + "created_at": "2023-01-01T12:00:00Z", + "author": "testuser", + } + + result = _build_json_reply("123456789", reply_info, verbose=False) + + assert result["id"] == "123456789" + assert result["success"] is True + assert result["reply_posted"] is True + assert result["reply_id"] == "987654321" + assert ( + result["reply_url"] + == "https://github.com/owner/repo/pull/1#discussion_r987654321" + ) + assert result["created_at"] == "2023-01-01T12:00:00Z" + assert result["author"] == "testuser" + + def test_verbose_json_reply(self): + """Test building verbose JSON reply.""" + reply_info = { + "reply_id": "987654321", + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321", + "created_at": "2023-01-01T12:00:00Z", + "author": "testuser", + "pr_number": "42", + "pr_title": "Add awesome feature", + "parent_comment_author": "reviewer123", + "body_preview": "Test reply", + "thread_url": "https://github.com/owner/repo/pull/42#pullrequestreview-123456", + "review_id": "123456", + } + + result = _build_json_reply( + "IC_kwDOABcD12MAAAABcDE3fg", reply_info, verbose=True + ) + + assert result["verbose"] is True + assert result["pr_number"] == "42" + assert result["pr_title"] == "Add awesome feature" + assert result["parent_comment_author"] == "reviewer123" + assert result["body_preview"] == "Test reply" + assert ( + result["thread_url"] + == "https://github.com/owner/repo/pull/42#pullrequestreview-123456" + ) + assert result["review_id"] == "123456" + + def test_optional_fields_handling(self): + """Test handling of optional fields in JSON reply.""" + reply_info = { + "reply_id": "987654321", + "pr_number": "", # Empty field should not be included + "pr_title": None, # None field should not be included + "parent_comment_author": "reviewer", # Non-empty field should be included + } + + result = _build_json_reply("123456789", reply_info, verbose=False) + + assert "pr_number" not in result + assert "pr_title" not in result + assert result["parent_comment_author"] == "reviewer" + + def test_minimal_json_reply(self): + """Test building JSON reply with minimal information.""" + reply_info = {} + + result = _build_json_reply("123456789", reply_info, verbose=False) + + assert result["id"] == "123456789" + assert result["success"] is True + assert result["reply_posted"] is True + assert result["reply_id"] == "" + assert result["reply_url"] == "" + assert result["created_at"] == "" + assert result["author"] == "" + + +class TestShowWarnings: + """Test the _show_warnings function.""" + + def test_mention_warning_pretty_mode(self, capsys): + """Test warning for replies starting with mention in pretty mode.""" + _show_warnings("@user thanks for the review!", pretty=True) + + captured = capsys.readouterr() + assert ( + "โš ๏ธ Note: Reply starts with '@' - this will mention users" in captured.err + ) + + def test_mention_warning_json_mode(self, capsys): + """Test no warning for mentions in JSON mode.""" + _show_warnings("@user thanks for the review!", pretty=False) + + captured = capsys.readouterr() + assert captured.err == "" + + def test_repetitive_content_warning(self, capsys): + """Test warning for repetitive content.""" + _show_warnings("aaaaaaaaaaaaa", pretty=True) + + captured = capsys.readouterr() + assert "โš ๏ธ Note: Reply contains very repetitive content" in captured.err + + def test_no_warnings_for_normal_content(self, capsys): + """Test no warnings for normal content.""" + _show_warnings("This is a normal reply with good content", pretty=True) + + captured = capsys.readouterr() + assert captured.err == "" + + def test_repetitive_content_short_text(self, capsys): + """Test no warning for short repetitive text.""" + _show_warnings("aaa", pretty=True) + + captured = capsys.readouterr() + assert "repetitive content" not in captured.err + + +class TestShowProgress: + """Test the _show_progress function.""" + + def test_progress_pretty_mode(self, capsys): + """Test progress messages in pretty mode.""" + _show_progress("123456789", "This is a test reply", pretty=True) + + captured = capsys.readouterr() + assert "๐Ÿ’ฌ Posting reply to 123456789" in captured.out + assert "๐Ÿ“ Reply: This is a test reply" in captured.out + + def test_progress_json_mode(self, capsys): + """Test no progress messages in JSON mode.""" + _show_progress("123456789", "This is a test reply", pretty=False) + + captured = capsys.readouterr() + assert captured.out == "" + + def test_progress_long_body_truncation(self, capsys): + """Test progress message with long body truncation.""" + long_body = "x" * 150 + _show_progress("123456789", long_body, pretty=True) + + captured = capsys.readouterr() + assert "๐Ÿ’ฌ Posting reply to 123456789" in captured.out + assert "๐Ÿ“ Reply: " + "x" * 100 + "..." in captured.out + + def test_progress_short_body_no_truncation(self, capsys): + """Test progress message with short body (no truncation).""" + short_body = "Short reply" + _show_progress("123456789", short_body, pretty=True) + + captured = capsys.readouterr() + assert "๐Ÿ“ Reply: Short reply" in captured.out + assert "..." not in captured.out + + +class TestShowIdHelp: + """Test the _show_id_help function.""" + + def test_id_help_content(self, capsys): + """Test that ID help shows comprehensive content.""" + ctx = Mock() + + _show_id_help(ctx) + + captured = capsys.readouterr() + assert "๐ŸŽฏ GitHub ID Types for Reply Command" in captured.out + assert "SUPPORTED ID TYPES:" in captured.out + assert "PRRT_kwDOABcD12MAAAABcDE3fg" in captured.out + assert "NOT SUPPORTED:" in captured.out + assert "PRRC_kwDOABcD12MAAAABcDE3fg" in captured.out + assert "HOW TO FIND THE RIGHT ID:" in captured.out + assert "toady fetch --pr --pretty" in captured.out + assert "BEST PRACTICES:" in captured.out + assert "EXAMPLES:" in captured.out + assert "TROUBLESHOOTING:" in captured.out + ctx.exit.assert_called_once_with(0) + + +class TestHandleReplyError: + """Test the _handle_reply_error function.""" + + def test_comment_not_found_error_pretty(self, capsys): + """Test CommentNotFoundError handling in pretty mode.""" + ctx = Mock() + error = CommentNotFoundError("Comment 999 not found in PR #1") + + _handle_reply_error(ctx, error, "999", pretty=True) + + captured = capsys.readouterr() + assert "โŒ Comment not found: Comment 999 not found in PR #1" in captured.err + assert "๐Ÿ’ก Possible causes:" in captured.err + ctx.exit.assert_called_with(1) + + def test_comment_not_found_error_json(self, capsys): + """Test CommentNotFoundError handling in JSON mode.""" + ctx = Mock() + error = CommentNotFoundError("Comment 999 not found in PR #1") + + _handle_reply_error(ctx, error, "999", pretty=False) + + captured = capsys.readouterr() + # Split by lines and take the first line which should be JSON + lines = captured.err.strip().split("\n") + output = json.loads(lines[0]) + assert output["success"] is False + assert output["reply_posted"] is False + assert output["error"] == "comment_not_found" + assert "Comment 999 not found" in output["error_message"] + # Note: ctx.exit is called multiple times due to implementation behavior + assert ctx.exit.call_count >= 1 + + def test_authentication_error_pretty(self, capsys): + """Test GitHubAuthenticationError handling in pretty mode.""" + ctx = Mock() + error = GitHubAuthenticationError("Authentication failed") + + _handle_reply_error(ctx, error, "123456789", pretty=True) + + captured = capsys.readouterr() + assert "โŒ Authentication failed: [GITHUB_AUTHENTICATION_ERROR]" in captured.err + assert "๐Ÿ’ก Try running: gh auth login" in captured.err + ctx.exit.assert_called_with(1) + + def test_authentication_error_json(self, capsys): + """Test GitHubAuthenticationError handling in JSON mode.""" + ctx = Mock() + error = GitHubAuthenticationError("Authentication failed") + + _handle_reply_error(ctx, error, "123456789", pretty=False) + + captured = capsys.readouterr() + lines = captured.err.strip().split("\n") + output = json.loads(lines[0]) + assert output["error"] == "authentication_failed" + assert "[GITHUB_AUTHENTICATION_ERROR]" in output["error_message"] + assert ctx.exit.call_count >= 1 + + def test_timeout_error_pretty(self, capsys): + """Test GitHubTimeoutError handling in pretty mode.""" + ctx = Mock() + error = GitHubTimeoutError("Request timed out") + + _handle_reply_error(ctx, error, "123456789", pretty=True) + + captured = capsys.readouterr() + assert "โŒ Request timed out: [GITHUB_TIMEOUT_ERROR]" in captured.err + assert "๐Ÿ’ก Try again in a moment" in captured.err + ctx.exit.assert_called_with(1) + + def test_rate_limit_error_pretty(self, capsys): + """Test GitHubRateLimitError handling in pretty mode.""" + ctx = Mock() + error = GitHubRateLimitError("Rate limit exceeded") + + _handle_reply_error(ctx, error, "123456789", pretty=True) + + captured = capsys.readouterr() + assert "โŒ Rate limit exceeded: [GITHUB_RATE_LIMIT_ERROR]" in captured.err + assert "๐Ÿ’ก You've made too many requests" in captured.err + ctx.exit.assert_called_with(1) + + def test_github_api_error_403_pretty(self, capsys): + """Test GitHubAPIError 403 handling in pretty mode.""" + ctx = Mock() + error = GitHubAPIError("403 Forbidden") + + _handle_reply_error(ctx, error, "123456789", pretty=True) + + captured = capsys.readouterr() + assert "โŒ Permission denied: [GITHUB_API_ERROR]" in captured.err + assert "๐Ÿ’ก Possible causes:" in captured.err + ctx.exit.assert_called_with(1) + + def test_github_api_error_403_json(self, capsys): + """Test GitHubAPIError 403 handling in JSON mode.""" + ctx = Mock() + error = GitHubAPIError("403 Forbidden") + + _handle_reply_error(ctx, error, "123456789", pretty=False) + + captured = capsys.readouterr() + lines = captured.err.strip().split("\n") + output = json.loads(lines[0]) + assert output["error"] == "permission_denied" + ctx.exit.assert_called_with(1) + + def test_github_api_error_general_pretty(self, capsys): + """Test general GitHubAPIError handling in pretty mode.""" + ctx = Mock() + error = GitHubAPIError("500 Internal Server Error") + + _handle_reply_error(ctx, error, "123456789", pretty=True) + + captured = capsys.readouterr() + assert "โŒ GitHub API error: [GITHUB_API_ERROR]" in captured.err + assert "๐Ÿ’ก This may be a temporary issue" in captured.err + ctx.exit.assert_called_with(1) + + def test_reply_service_error_pretty(self, capsys): + """Test ReplyServiceError handling in pretty mode.""" + ctx = Mock() + error = ReplyServiceError("Service error") + + _handle_reply_error(ctx, error, "123456789", pretty=True) + + captured = capsys.readouterr() + assert "โŒ Failed to post reply: Service error" in captured.err + assert "๐Ÿ’ก This is likely a service error" in captured.err + ctx.exit.assert_called_once_with(1) + + def test_reply_service_error_json(self, capsys): + """Test ReplyServiceError handling in JSON mode.""" + ctx = Mock() + error = ReplyServiceError("Service error") + + _handle_reply_error(ctx, error, "123456789", pretty=False) + + captured = capsys.readouterr() + output = json.loads(captured.err) + assert output["error"] == "api_error" + assert output["error_message"] == "Service error" + ctx.exit.assert_called_once_with(1) + + def test_unexpected_error_handling(self, capsys): + """Test handling of unexpected errors.""" + ctx = Mock() + error = ValueError("Unexpected error") + + _handle_reply_error(ctx, error, "123456789", pretty=False) + + captured = capsys.readouterr() + output = json.loads(captured.err) + assert output["error"] == "api_error" + assert output["error_message"] == "Unexpected error" + ctx.exit.assert_called_once_with(1) + + +class TestReplyCommandIntegration: + """Test integration of reply command components.""" + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_successful_reply_json_format( + self, mock_resolve_format, mock_service_class, runner + ): + """Test successful reply with JSON format.""" + mock_service = Mock() + mock_service.post_reply.return_value = { + "reply_id": "987654321", + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321", + "comment_id": "123456789", + "created_at": "2023-01-01T12:00:00Z", + "author": "testuser", + } + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + result = runner.invoke( + cli, ["reply", "--id", "123456789", "--body", "Test reply"] + ) + + assert result.exit_code == 0 + output = json.loads(result.output) + assert output["reply_posted"] is True + assert output["reply_id"] == "987654321" + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_successful_reply_pretty_format( + self, mock_resolve_format, mock_service_class, runner, capsys + ): + """Test successful reply with pretty format.""" + mock_service = Mock() + mock_service.post_reply.return_value = { + "reply_id": "987654321", + "reply_url": "https://github.com/owner/repo/pull/1#discussion_r987654321", + } + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + result = runner.invoke( + cli, ["reply", "--id", "123456789", "--body", "Test reply", "--pretty"] + ) + + assert result.exit_code == 0 + assert "โœ… Reply posted successfully" in result.output + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_verbose_mode_integration( + self, mock_resolve_format, mock_service_class, runner + ): + """Test verbose mode integration.""" + mock_service = Mock() + mock_service.post_reply.return_value = { + "reply_id": "987654321", + "pr_number": "42", + "pr_title": "Test PR", + } + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke( + cli, ["reply", "--id", "123456789", "--body", "Test", "--verbose"] + ) + + # Verify that fetch_context=True was passed to the service + fetch_context = mock_service.post_reply.call_args[1]["fetch_context"] + assert fetch_context is True + + def test_missing_required_options(self, runner): + """Test error handling for missing required options.""" + # Missing both options + result = runner.invoke(cli, ["reply"]) + assert result.exit_code != 0 + assert "Missing option '--id'" in result.output + + # Missing body + result = runner.invoke(cli, ["reply", "--id", "123"]) + assert result.exit_code != 0 + assert "Missing option '--body'" in result.output + + # Missing id + result = runner.invoke(cli, ["reply", "--body", "test"]) + assert result.exit_code != 0 + assert "Missing option '--id'" in result.output + + def test_help_ids_flag(self, runner, capsys): + """Test --help-ids flag integration.""" + result = runner.invoke(cli, ["reply", "--help-ids"]) + + assert result.exit_code == 0 + assert "๐ŸŽฏ GitHub ID Types for Reply Command" in result.output + + @patch("toady.commands.reply.resolve_format_from_options") + def test_format_resolution_error_handling(self, mock_resolve_format, runner): + """Test format resolution error handling.""" + mock_resolve_format.side_effect = Exception("Format error") + + result = runner.invoke(cli, ["reply", "--id", "123", "--body", "test"]) + + assert result.exit_code == 1 + assert "Error: Format error" in result.output + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_service_request_creation( + self, mock_resolve_format, mock_service_class, runner + ): + """Test that ReplyRequest is created correctly.""" + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["reply", "--id", "123456789", "--body", "Test reply"]) + + # Verify ReplyRequest was created with correct parameters + request = mock_service.post_reply.call_args[0][0] + assert isinstance(request, ReplyRequest) + assert request.comment_id == "123456789" + assert request.reply_body == "Test reply" + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_warning_display_integration( + self, mock_resolve_format, mock_service_class, runner + ): + """Test warning display integration in pretty mode.""" + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + result = runner.invoke( + cli, ["reply", "--id", "123456789", "--body", "@user thanks", "--pretty"] + ) + + assert "โš ๏ธ Note: Reply starts with '@'" in result.output + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_progress_display_integration( + self, mock_resolve_format, mock_service_class, runner + ): + """Test progress display integration in pretty mode.""" + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "pretty" + + result = runner.invoke( + cli, ["reply", "--id", "123456789", "--body", "Test reply", "--pretty"] + ) + + assert "๐Ÿ’ฌ Posting reply to 123456789" in result.output + assert "๐Ÿ“ Reply: Test reply" in result.output + + +class TestReplyCommandEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_body_at_exact_limits(self, runner): + """Test body validation at exact character limits.""" + # Test minimum valid length (3 characters) + with patch("toady.commands.reply.ReplyService") as mock_service_class: + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + + result = runner.invoke(cli, ["reply", "--id", "123456789", "--body", "abc"]) + assert result.exit_code == 0 + + # Test maximum valid length (65536 characters) + with patch("toady.commands.reply.ReplyService") as mock_service_class: + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + + max_body = "x" * 65536 + result = runner.invoke( + cli, ["reply", "--id", "123456789", "--body", max_body] + ) + assert result.exit_code == 0 + + def test_various_id_formats(self, runner): + """Test various ID format validations.""" + test_cases = [ + ("0", False), # Invalid: zero + ("123456789012345678901", False), # Invalid: too many digits + ("IC_abc", False), # Invalid: too short + ("IC_" + "a" * 101, False), # Invalid: too long + ("IC_kwDO@#$%", False), # Invalid: bad characters + ("PRRC_kwDOABcD12MAAAABcDE3fg", False), # Invalid: PRRC not allowed + ("RT_kwDOABcD12MAAAABcDE3fg", True), # Valid: thread ID + ("123456789", True), # Valid: numeric ID + ] + + for test_id, should_succeed in test_cases: + if should_succeed: + with patch("toady.commands.reply.ReplyService") as mock_service_class: + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + + result = runner.invoke( + cli, ["reply", "--id", test_id, "--body", "test"] + ) + assert result.exit_code == 0, f"Should succeed for ID: {test_id}" + else: + result = runner.invoke( + cli, ["reply", "--id", test_id, "--body", "test"] + ) + assert result.exit_code != 0, f"Should fail for ID: {test_id}" + + def test_format_output_edge_cases(self): + """Test format output with edge case data.""" + # Test with empty reply info + result = _build_json_reply("123", {}, False) + assert result["id"] == "123" + assert result["reply_id"] == "" + + # Test with None values - the function uses .get() which returns None + # when key exists with None + reply_info = {"reply_id": None, "author": None} + result = _build_json_reply("123", reply_info, False) + # The function uses .get() which returns None if the key exists with None value + assert result["reply_id"] is None + assert result["author"] is None + + @patch("toady.commands.reply.ReplyService") + @patch("toady.commands.reply.resolve_format_from_options") + def test_unicode_body_handling( + self, mock_resolve_format, mock_service_class, runner + ): + """Test handling of Unicode characters in reply body.""" + mock_service = Mock() + mock_service.post_reply.return_value = {"reply_id": "123"} + mock_service_class.return_value = mock_service + mock_resolve_format.return_value = "json" + + unicode_body = "Test with รฉmojis ๐ŸŽ‰ and spรซcial characters" + result = runner.invoke( + cli, ["reply", "--id", "123456789", "--body", unicode_body] + ) + + assert result.exit_code == 0 + # Verify the Unicode body was passed correctly + request = mock_service.post_reply.call_args[0][0] + assert request.reply_body == unicode_body + + +@pytest.fixture(scope="module") +def runner(): + """Create a Click CLI test runner for the module.""" + return CliRunner() diff --git a/tests/unit/commands/test_resolve.py b/tests/unit/commands/test_resolve.py new file mode 100644 index 0000000..bd8f24e --- /dev/null +++ b/tests/unit/commands/test_resolve.py @@ -0,0 +1,1005 @@ +"""Unit tests for the resolve command module. + +This module tests the core resolve command logic, including parameter validation, +error handling, format resolution, and service integration. It focuses on unit +testing the command implementation without testing the CLI interface directly. +""" + +import json +from unittest.mock import Mock, patch + +import click +from click.testing import CliRunner +import pytest + +from toady.cli import cli +from toady.commands.resolve import ( + _display_summary, + _fetch_and_filter_threads, + _get_action_labels, + _handle_bulk_resolve, + _handle_bulk_resolve_error, + _handle_confirmation_prompt, + _handle_empty_threads, + _handle_single_resolve, + _handle_single_resolve_error, + _handle_single_resolve_success, + _process_threads, + _show_single_resolve_progress, + _validate_and_prepare_thread_id, + _validate_resolve_parameters, + resolve, +) +from toady.exceptions import ( + GitHubAPIError, + GitHubAuthenticationError, + GitHubRateLimitError, + ResolveServiceError, + ThreadNotFoundError, + ThreadPermissionError, +) +from toady.services.fetch_service import FetchServiceError + + +class TestResolveCommandCore: + """Test the core resolve command functionality.""" + + def test_resolve_command_exists(self): + """Test that the resolve command is properly defined.""" + assert resolve is not None + assert callable(resolve) + assert hasattr(resolve, "params") + + def test_resolve_command_parameters(self): + """Test that resolve command has expected parameters.""" + param_names = [param.name for param in resolve.params] + expected_params = [ + "thread_id", + "bulk_resolve", + "pr_number", + "undo", + "yes", + "format", + "pretty", + "limit", + ] + + for expected_param in expected_params: + assert expected_param in param_names, f"Missing parameter: {expected_param}" + + def test_resolve_command_defaults(self): + """Test resolve command parameter defaults.""" + param_defaults = {param.name: param.default for param in resolve.params} + + assert param_defaults["thread_id"] is None + assert param_defaults["bulk_resolve"] is False + assert param_defaults["pr_number"] is None + assert param_defaults["undo"] is False + assert param_defaults["yes"] is False + assert param_defaults["format"] is None + assert param_defaults["pretty"] is False + assert param_defaults["limit"] == 100 + + +class TestValidateResolveParameters: + """Test parameter validation in the resolve command.""" + + def test_validate_mutually_exclusive_bulk_and_thread_id(self): + """Test that bulk_resolve and thread_id are mutually exclusive.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_resolve_parameters(True, "thread123", 456, 100) + assert "Cannot use --all and --thread-id together" in str(exc_info.value) + + def test_validate_requires_thread_id_or_bulk(self): + """Test that either thread_id or bulk_resolve must be specified.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_resolve_parameters(False, None, None, 100) + assert "Must specify either --thread-id or --all" in str(exc_info.value) + + @patch("toady.commands.resolve.validate_pr_number") + def test_validate_pr_number_called_when_provided(self, mock_validate_pr): + """Test that PR number validation is called when provided.""" + _validate_resolve_parameters(True, None, 123, 100) + mock_validate_pr.assert_called_once_with(123) + + @patch("toady.commands.resolve.validate_pr_number") + def test_validate_pr_number_not_called_when_none(self, mock_validate_pr): + """Test that PR number validation is not called when None.""" + _validate_resolve_parameters(False, "thread123", None, 100) + mock_validate_pr.assert_not_called() + + def test_validate_bulk_requires_pr_number(self): + """Test that bulk resolve requires PR number.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_resolve_parameters(True, None, None, 100) + assert "--pr is required when using --all" in str(exc_info.value) + + @patch("toady.command_utils.validate_limit") + def test_validate_limit_called(self, mock_validate_limit): + """Test that limit validation is called.""" + _validate_resolve_parameters(False, "thread123", None, 150) + mock_validate_limit.assert_called_once_with(150, max_limit=1000) + + +class TestValidateAndPrepareThreadId: + """Test thread ID validation and preparation.""" + + def test_valid_thread_id_stripped_and_returned(self): + """Test that valid thread ID is stripped and returned.""" + with patch("toady.commands.resolve.validate_thread_id") as mock_validate: + result = _validate_and_prepare_thread_id(" thread123 ") + assert result == "thread123" + mock_validate.assert_called_once_with("thread123") + + def test_empty_thread_id_raises_error(self): + """Test that empty thread ID raises error.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_and_prepare_thread_id("") + assert "Thread ID cannot be empty" in str(exc_info.value) + + def test_whitespace_thread_id_raises_error(self): + """Test that whitespace-only thread ID raises error.""" + with pytest.raises(click.BadParameter) as exc_info: + _validate_and_prepare_thread_id(" ") + assert "Thread ID cannot be empty" in str(exc_info.value) + + def test_validation_error_converted_to_bad_parameter(self): + """Test that validation errors are converted to BadParameter.""" + with patch("toady.commands.resolve.validate_thread_id") as mock_validate: + mock_validate.side_effect = ValueError("Invalid format") + with pytest.raises(click.BadParameter) as exc_info: + _validate_and_prepare_thread_id("invalid") + assert "Invalid format" in str(exc_info.value) + + +class TestFetchAndFilterThreads: + """Test thread fetching and filtering logic.""" + + @patch("toady.commands.resolve.FetchService") + @patch("toady.commands.resolve.click.echo") + def test_fetch_unresolved_threads_pretty(self, mock_echo, mock_service_class): + """Test fetching unresolved threads with pretty output.""" + mock_service = Mock() + mock_threads = [Mock(is_resolved=False), Mock(is_resolved=True)] + mock_service.fetch_review_threads_from_current_repo.return_value = mock_threads + mock_service_class.return_value = mock_service + + result = _fetch_and_filter_threads(123, False, True, 50) + + mock_echo.assert_called_once_with( + "๐Ÿ” Fetching threads from PR #123 (limit: 50)..." + ) + mock_service.fetch_review_threads_from_current_repo.assert_called_once_with( + pr_number=123, include_resolved=False, limit=50 + ) + assert len(result) == 1 # Only unresolved thread + + @patch("toady.commands.resolve.FetchService") + def test_fetch_resolved_threads_for_undo(self, mock_service_class): + """Test fetching resolved threads for undo operation.""" + mock_service = Mock() + mock_threads = [Mock(is_resolved=False), Mock(is_resolved=True)] + mock_service.fetch_review_threads_from_current_repo.return_value = mock_threads + mock_service_class.return_value = mock_service + + result = _fetch_and_filter_threads(123, True, False, 100) + + mock_service.fetch_review_threads_from_current_repo.assert_called_once_with( + pr_number=123, include_resolved=True, limit=100 + ) + assert len(result) == 1 # Only resolved thread + + @patch("toady.commands.resolve.FetchService") + def test_fetch_threads_no_pretty(self, mock_service_class): + """Test fetching threads without pretty output.""" + mock_service = Mock() + mock_threads = [Mock(is_resolved=False)] + mock_service.fetch_review_threads_from_current_repo.return_value = mock_threads + mock_service_class.return_value = mock_service + + with patch("toady.commands.resolve.click.echo") as mock_echo: + _fetch_and_filter_threads(123, False, False, 100) + mock_echo.assert_not_called() + + +class TestHandleConfirmationPrompt: + """Test confirmation prompt handling.""" + + def test_confirmation_skipped_with_yes_flag(self): + """Test that confirmation is skipped when yes flag is used.""" + ctx = Mock() + threads = [Mock(thread_id="t1", title="Title 1")] + + # Should not exit when yes=True + _handle_confirmation_prompt(ctx, threads, "resolve", "๐Ÿ”’", 123, True, True) + ctx.exit.assert_not_called() + + @patch("toady.commands.resolve.click.confirm") + @patch("toady.commands.resolve.click.echo") + def test_confirmation_accepted_pretty(self, mock_echo, mock_confirm): + """Test confirmation accepted in pretty mode.""" + mock_confirm.return_value = True + ctx = Mock() + threads = [Mock(thread_id="t1", title="Title 1")] + + _handle_confirmation_prompt(ctx, threads, "resolve", "๐Ÿ”’", 123, False, True) + + mock_confirm.assert_called_once_with("Do you want to resolve these threads?") + ctx.exit.assert_not_called() + + @patch("toady.commands.resolve.click.confirm") + @patch("toady.commands.resolve.click.echo") + def test_confirmation_declined_pretty(self, mock_echo, mock_confirm): + """Test confirmation declined in pretty mode.""" + mock_confirm.return_value = False + ctx = Mock() + threads = [Mock(thread_id="t1", title="Title 1")] + + _handle_confirmation_prompt(ctx, threads, "resolve", "๐Ÿ”’", 123, False, True) + + mock_confirm.assert_called_once_with("Do you want to resolve these threads?") + ctx.exit.assert_called_once_with(0) + + @patch("toady.commands.resolve.click.echo") + def test_confirmation_required_json_mode(self, mock_echo): + """Test that confirmation is required in JSON mode without yes flag.""" + ctx = Mock() + threads = [Mock(thread_id="t1", title="Title 1")] + + _handle_confirmation_prompt(ctx, threads, "resolve", "๐Ÿ”’", 123, False, False) + + ctx.exit.assert_called_once_with(1) + mock_echo.assert_called_once() + + @patch("toady.commands.resolve.click.confirm") + @patch("toady.commands.resolve.click.echo") + def test_confirmation_shows_first_five_threads(self, mock_echo, mock_confirm): + """Test that confirmation shows first 5 threads and indicates more.""" + mock_confirm.return_value = True + ctx = Mock() + threads = [Mock(thread_id=f"t{i}", title=f"Title {i}") for i in range(10)] + + _handle_confirmation_prompt(ctx, threads, "resolve", "๐Ÿ”’", 123, False, True) + + # Check that it shows first 5 and indicates more + echo_calls = [ + str(call.args[0]) if call.args else str(call) + for call in mock_echo.call_args_list + ] + assert any("1. t0 - Title 0" in call for call in echo_calls) + assert any("5. t4 - Title 4" in call for call in echo_calls) + assert any("... and 5 more" in call for call in echo_calls) + + +class TestProcessThreads: + """Test thread processing logic.""" + + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve.time.sleep") + @patch("toady.commands.resolve.click.echo") + def test_process_threads_resolve_success( + self, mock_echo, mock_sleep, mock_service_class + ): + """Test successful thread resolution processing.""" + mock_service = Mock() + mock_service_class.return_value = mock_service + + threads = [Mock(thread_id="t1"), Mock(thread_id="t2")] + succeeded, failed, failed_threads = _process_threads( + threads, False, "Resolving", "๐Ÿ”’", True + ) + + assert succeeded == 2 + assert failed == 0 + assert failed_threads == [] + assert mock_service.resolve_thread.call_count == 2 + mock_service.resolve_thread.assert_any_call("t1") + mock_service.resolve_thread.assert_any_call("t2") + + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve.time.sleep") + @patch("toady.commands.resolve.click.echo") + def test_process_threads_unresolve_success( + self, mock_echo, mock_sleep, mock_service_class + ): + """Test successful thread unresolve processing.""" + mock_service = Mock() + mock_service_class.return_value = mock_service + + threads = [Mock(thread_id="t1")] + succeeded, failed, failed_threads = _process_threads( + threads, True, "Unresolving", "๐Ÿ”“", False + ) + + assert succeeded == 1 + assert failed == 0 + mock_service.unresolve_thread.assert_called_once_with("t1") + + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve.time.sleep") + @patch("toady.commands.resolve.click.echo") + def test_process_threads_rate_limit_error( + self, mock_echo, mock_sleep, mock_service_class + ): + """Test handling of rate limit errors during processing.""" + mock_service = Mock() + mock_service.resolve_thread.side_effect = [ + None, # First call succeeds + GitHubRateLimitError("Rate limit exceeded"), # Second call fails + ] + mock_service_class.return_value = mock_service + + threads = [Mock(thread_id="t1"), Mock(thread_id="t2")] + succeeded, failed, failed_threads = _process_threads( + threads, False, "Resolving", "๐Ÿ”’", True + ) + + assert succeeded == 1 + assert failed == 1 + assert len(failed_threads) == 1 + assert failed_threads[0]["thread_id"] == "t2" + assert "Rate limit exceeded" in failed_threads[0]["error"] + + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve.time.sleep") + @patch("toady.commands.resolve.click.echo") + def test_process_threads_api_error(self, mock_echo, mock_sleep, mock_service_class): + """Test handling of API errors during processing.""" + mock_service = Mock() + mock_service.resolve_thread.side_effect = ResolveServiceError("API error") + mock_service_class.return_value = mock_service + + threads = [Mock(thread_id="t1")] + succeeded, failed, failed_threads = _process_threads( + threads, False, "Resolving", "๐Ÿ”’", False + ) + + assert succeeded == 0 + assert failed == 1 + assert failed_threads[0]["thread_id"] == "t1" + + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve.time.sleep") + def test_process_threads_no_sleep_after_last(self, mock_sleep, mock_service_class): + """Test that no sleep occurs after the last thread.""" + mock_service = Mock() + mock_service_class.return_value = mock_service + + threads = [Mock(thread_id="t1")] + _process_threads(threads, False, "Resolving", "๐Ÿ”’", False) + + # Should not sleep after processing the last (only) thread + mock_sleep.assert_not_called() + + +class TestDisplaySummary: + """Test summary display logic.""" + + @patch("toady.commands.resolve.click.echo") + def test_display_summary_pretty_success(self, mock_echo): + """Test summary display in pretty mode with success.""" + threads = [Mock(), Mock()] + _display_summary(threads, 2, 0, [], "resolve", "resolved", 123, True) + + echo_calls = [call[0][0] for call in mock_echo.call_args_list] + assert any("โœ… Bulk resolve completed:" in call for call in echo_calls) + assert any("๐Ÿ“Š Total threads processed: 2" in call for call in echo_calls) + assert any("โœ… Successfully resolved: 2" in call for call in echo_calls) + + @patch("toady.commands.resolve.click.echo") + def test_display_summary_pretty_with_failures(self, mock_echo): + """Test summary display in pretty mode with failures.""" + threads = [Mock(), Mock()] + failed_threads = [{"thread_id": "t1", "error": "Failed"}] + _display_summary( + threads, 1, 1, failed_threads, "resolve", "resolved", 123, True + ) + + echo_calls = [call[0][0] for call in mock_echo.call_args_list] + assert any("โŒ Failed: 1" in call for call in echo_calls) + assert any("โŒ Failed threads:" in call for call in echo_calls) + assert any("โ€ข t1: Failed" in call for call in echo_calls) + + @patch("toady.commands.resolve.click.echo") + def test_display_summary_json_success(self, mock_echo): + """Test summary display in JSON mode with success.""" + threads = [Mock(), Mock()] + _display_summary(threads, 2, 0, [], "resolve", "resolved", 123, False) + + # Should output JSON + mock_echo.assert_called_once() + output = json.loads(mock_echo.call_args[0][0]) + assert output["pr_number"] == 123 + assert output["action"] == "resolve" + assert output["threads_processed"] == 2 + assert output["threads_succeeded"] == 2 + assert output["threads_failed"] == 0 + assert output["success"] is True + + @patch("toady.commands.resolve.click.echo") + def test_display_summary_json_with_failures(self, mock_echo): + """Test summary display in JSON mode with failures.""" + threads = [Mock()] + failed_threads = [{"thread_id": "t1", "error": "Failed"}] + _display_summary( + threads, 0, 1, failed_threads, "unresolve", "unresolved", 456, False + ) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["success"] is False + assert output["failed_threads"] == failed_threads + + +class TestGetActionLabels: + """Test action label generation.""" + + def test_get_action_labels_resolve(self): + """Test action labels for resolve operation.""" + action, action_past, action_present, action_symbol = _get_action_labels(False) + assert action == "resolve" + assert action_past == "resolved" + assert action_present == "Resolving" + assert action_symbol == "๐Ÿ”’" + + def test_get_action_labels_unresolve(self): + """Test action labels for unresolve operation.""" + action, action_past, action_present, action_symbol = _get_action_labels(True) + assert action == "unresolve" + assert action_past == "unresolved" + assert action_present == "Unresolving" + assert action_symbol == "๐Ÿ”“" + + +class TestHandleEmptyThreads: + """Test empty threads handling.""" + + @patch("toady.commands.resolve.click.echo") + def test_handle_empty_threads_pretty_resolve(self, mock_echo): + """Test empty threads handling in pretty mode for resolve.""" + _handle_empty_threads(123, "resolve", False, True) + mock_echo.assert_called_once_with("โœ… No unresolved threads found in PR #123") + + @patch("toady.commands.resolve.click.echo") + def test_handle_empty_threads_pretty_unresolve(self, mock_echo): + """Test empty threads handling in pretty mode for unresolve.""" + _handle_empty_threads(456, "unresolve", True, True) + mock_echo.assert_called_once_with("โœ… No resolved threads found in PR #456") + + @patch("toady.commands.resolve.click.echo") + def test_handle_empty_threads_json_resolve(self, mock_echo): + """Test empty threads handling in JSON mode for resolve.""" + _handle_empty_threads(123, "resolve", False, False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["pr_number"] == 123 + assert output["action"] == "resolve" + assert output["threads_processed"] == 0 + assert output["success"] is True + assert output["message"] == "No unresolved threads found" + + @patch("toady.commands.resolve.click.echo") + def test_handle_empty_threads_json_unresolve(self, mock_echo): + """Test empty threads handling in JSON mode for unresolve.""" + _handle_empty_threads(456, "unresolve", True, False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["message"] == "No resolved threads found" + + +class TestHandleBulkResolveError: + """Test bulk resolve error handling.""" + + @patch("toady.commands.resolve.click.echo") + def test_handle_fetch_service_error_pretty(self, mock_echo): + """Test FetchServiceError handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = FetchServiceError("Fetch failed") + + with pytest.raises(SystemExit): + _handle_bulk_resolve_error(ctx, error, 123, "resolve", True) + + mock_echo.assert_called_with( + "โŒ Failed to fetch threads: Fetch failed", err=True + ) + ctx.exit.assert_called_once_with(1) + + @patch("toady.commands.resolve.click.echo") + def test_handle_fetch_service_error_json(self, mock_echo): + """Test FetchServiceError handling in JSON mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = FetchServiceError("Fetch failed") + + with pytest.raises(SystemExit): + _handle_bulk_resolve_error(ctx, error, 123, "resolve", False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["error"] == "fetch_failed" + assert output["error_message"] == "Fetch failed" + + @patch("toady.commands.resolve.click.echo") + def test_handle_authentication_error_pretty(self, mock_echo): + """Test GitHubAuthenticationError handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = GitHubAuthenticationError("Auth failed") + + with pytest.raises(SystemExit): + _handle_bulk_resolve_error(ctx, error, 123, "resolve", True) + + calls = [ + str(call.args[0]) if call.args else str(call) + for call in mock_echo.call_args_list + ] + assert any( + "โŒ Authentication failed: [GITHUB_AUTHENTICATION_ERROR] Auth failed" + in call + for call in calls + ) + assert any("๐Ÿ’ก Try running: gh auth login" in call for call in calls) + + def test_handle_click_exit_exception(self): + """Test that Click Exit exceptions are re-raised.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = click.exceptions.Exit(5) + + with pytest.raises(SystemExit): + _handle_bulk_resolve_error(ctx, error, 123, "resolve", True) + + # The function should call ctx.exit(5) to re-raise the exit + ctx.exit.assert_called_once_with(5) + + @patch("toady.commands.resolve.click.echo") + def test_handle_unexpected_error_pretty(self, mock_echo): + """Test unexpected error handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = ValueError("Unexpected error") + + with pytest.raises(SystemExit): + _handle_bulk_resolve_error(ctx, error, 123, "resolve", True) + + mock_echo.assert_called_with( + "โŒ Unexpected error during bulk resolve: Unexpected error", err=True + ) + ctx.exit.assert_called_once_with(1) + + @patch("toady.commands.resolve.click.echo") + def test_handle_unexpected_error_json(self, mock_echo): + """Test unexpected error handling in JSON mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = ValueError("Unexpected error") + + with pytest.raises(SystemExit): + _handle_bulk_resolve_error(ctx, error, 123, "resolve", False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["error"] == "internal_error" + assert output["error_message"] == "Unexpected error" + + +class TestShowSingleResolveProgress: + """Test single resolve progress display.""" + + @patch("toady.commands.resolve.click.echo") + def test_show_progress_resolve_pretty(self, mock_echo): + """Test progress display for resolve in pretty mode.""" + _show_single_resolve_progress("thread123", False, True) + mock_echo.assert_called_once_with("๐Ÿ”’ Resolving thread thread123") + + @patch("toady.commands.resolve.click.echo") + def test_show_progress_unresolve_pretty(self, mock_echo): + """Test progress display for unresolve in pretty mode.""" + _show_single_resolve_progress("thread123", True, True) + mock_echo.assert_called_once_with("๐Ÿ”“ Unresolving thread thread123") + + @patch("toady.commands.resolve.click.echo") + def test_show_progress_no_pretty(self, mock_echo): + """Test no progress display when pretty=False.""" + _show_single_resolve_progress("thread123", False, False) + mock_echo.assert_not_called() + + +class TestHandleSingleResolveSuccess: + """Test single resolve success handling.""" + + @patch("toady.commands.resolve.click.echo") + def test_handle_success_resolve_pretty(self, mock_echo): + """Test success handling for resolve in pretty mode.""" + result = { + "thread_id": "thread123", + "action": "resolve", + "success": True, + "thread_url": "https://github.com/test/repo/pull/1#discussion_r123", + } + _handle_single_resolve_success(result, False, True) + + calls = [call[0][0] for call in mock_echo.call_args_list] + assert any("โœ… Thread resolved successfully" in call for call in calls) + assert any("๐Ÿ”— View thread at:" in call for call in calls) + + @patch("toady.commands.resolve.click.echo") + def test_handle_success_unresolve_pretty(self, mock_echo): + """Test success handling for unresolve in pretty mode.""" + result = {"thread_id": "thread123", "action": "unresolve", "success": True} + _handle_single_resolve_success(result, True, True) + + mock_echo.assert_called_once_with("โœ… Thread unresolved successfully") + + @patch("toady.commands.resolve.click.echo") + def test_handle_success_json_mode(self, mock_echo): + """Test success handling in JSON mode.""" + result = {"thread_id": "thread123", "action": "resolve", "success": True} + _handle_single_resolve_success(result, False, False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output == result + + +class TestHandleSingleResolveError: + """Test single resolve error handling.""" + + @patch("toady.commands.resolve.click.echo") + def test_handle_thread_not_found_error_pretty(self, mock_echo): + """Test ThreadNotFoundError handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = ThreadNotFoundError("Thread not found") + + with pytest.raises(SystemExit): + _handle_single_resolve_error(ctx, error, "thread123", False, True) + + mock_echo.assert_called_with( + "โŒ Thread not found: [THREAD_NOT_FOUND] Thread not found", err=True + ) + ctx.exit.assert_called_once_with(1) + + @patch("toady.commands.resolve.click.echo") + def test_handle_thread_not_found_error_json(self, mock_echo): + """Test ThreadNotFoundError handling in JSON mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = ThreadNotFoundError("Thread not found") + + with pytest.raises(SystemExit): + _handle_single_resolve_error(ctx, error, "thread123", False, False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["error"] == "thread_not_found" + assert output["thread_id"] == "thread123" + assert output["action"] == "resolve" + + @patch("toady.commands.resolve.click.echo") + def test_handle_permission_error_pretty(self, mock_echo): + """Test ThreadPermissionError handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = ThreadPermissionError("Permission denied") + + with pytest.raises(SystemExit): + _handle_single_resolve_error(ctx, error, "thread123", False, True) + + calls = [ + str(call.args[0]) if call.args else str(call) + for call in mock_echo.call_args_list + ] + assert any( + "โŒ Permission denied: [GITHUB_PERMISSION_ERROR] Permission denied" in call + for call in calls + ) + assert any("๐Ÿ’ก Ensure you have write access" in call for call in calls) + + @patch("toady.commands.resolve.click.echo") + def test_handle_authentication_error_pretty(self, mock_echo): + """Test GitHubAuthenticationError handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = GitHubAuthenticationError("Auth failed") + + with pytest.raises(SystemExit): + _handle_single_resolve_error(ctx, error, "thread123", False, True) + + calls = [ + str(call.args[0]) if call.args else str(call) + for call in mock_echo.call_args_list + ] + assert any( + "โŒ Authentication failed: [GITHUB_AUTHENTICATION_ERROR] Auth failed" + in call + for call in calls + ) + assert any("๐Ÿ’ก Try running: gh auth login" in call for call in calls) + + @patch("toady.commands.resolve.click.echo") + def test_handle_resolve_service_error_pretty(self, mock_echo): + """Test ResolveServiceError handling in pretty mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = ResolveServiceError("Service error") + + with pytest.raises(SystemExit): + _handle_single_resolve_error(ctx, error, "thread123", False, True) + + mock_echo.assert_called_with( + "โŒ Failed to resolve thread: [RESOLVE_SERVICE_ERROR] Service error", + err=True, + ) + ctx.exit.assert_called_once_with(1) + + @patch("toady.commands.resolve.click.echo") + def test_handle_github_api_error_json(self, mock_echo): + """Test GitHubAPIError handling in JSON mode.""" + ctx = Mock() + ctx.exit.side_effect = SystemExit + error = GitHubAPIError("API error") + + with pytest.raises(SystemExit): + _handle_single_resolve_error(ctx, error, "thread123", True, False) + + output = json.loads(mock_echo.call_args[0][0]) + assert output["error"] == "api_error" + assert output["action"] == "unresolve" + + +class TestHandleSingleResolve: + """Test single resolve handling integration.""" + + @patch("toady.commands.resolve._validate_and_prepare_thread_id") + @patch("toady.commands.resolve._show_single_resolve_progress") + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve._handle_single_resolve_success") + def test_handle_single_resolve_success_flow( + self, + mock_handle_success, + mock_service_class, + mock_show_progress, + mock_validate_id, + ): + """Test successful single resolve flow.""" + ctx = Mock() + mock_validate_id.return_value = "clean_thread_id" + mock_service = Mock() + mock_result = {"thread_id": "clean_thread_id", "success": True} + mock_service.resolve_thread.return_value = mock_result + mock_service_class.return_value = mock_service + + _handle_single_resolve(ctx, " thread_id ", False, True) + + mock_validate_id.assert_called_once_with(" thread_id ") + mock_show_progress.assert_called_once_with("clean_thread_id", False, True) + mock_service.resolve_thread.assert_called_once_with("clean_thread_id") + mock_handle_success.assert_called_once_with(mock_result, False, True) + + @patch("toady.commands.resolve._validate_and_prepare_thread_id") + @patch("toady.commands.resolve._show_single_resolve_progress") + @patch("toady.commands.resolve.ResolveService") + @patch("toady.commands.resolve._handle_single_resolve_error") + def test_handle_single_resolve_error_flow( + self, + mock_handle_error, + mock_service_class, + mock_show_progress, + mock_validate_id, + ): + """Test error handling in single resolve flow.""" + ctx = Mock() + mock_validate_id.return_value = "clean_thread_id" + mock_service = Mock() + mock_error = ResolveServiceError("Service error") + mock_service.unresolve_thread.side_effect = mock_error + mock_service_class.return_value = mock_service + + _handle_single_resolve(ctx, "thread_id", True, False) + + mock_service.unresolve_thread.assert_called_once_with("clean_thread_id") + mock_handle_error.assert_called_once_with( + ctx, mock_error, "clean_thread_id", True, False + ) + + +class TestHandleBulkResolve: + """Test bulk resolve handling integration.""" + + @patch("toady.commands.resolve._fetch_and_filter_threads") + @patch("toady.commands.resolve._handle_empty_threads") + def test_handle_bulk_resolve_empty_threads(self, mock_handle_empty, mock_fetch): + """Test bulk resolve with no threads found.""" + ctx = Mock() + mock_fetch.return_value = [] + + _handle_bulk_resolve(ctx, 123, False, False, True, 100) + + mock_fetch.assert_called_once_with(123, False, True, 100) + mock_handle_empty.assert_called_once_with(123, "resolve", False, True) + + @patch("toady.commands.resolve._fetch_and_filter_threads") + @patch("toady.commands.resolve._handle_confirmation_prompt") + @patch("toady.commands.resolve._process_threads") + @patch("toady.commands.resolve._display_summary") + def test_handle_bulk_resolve_success_flow( + self, mock_display, mock_process, mock_confirm, mock_fetch + ): + """Test successful bulk resolve flow.""" + ctx = Mock() + mock_threads = [Mock(thread_id="t1"), Mock(thread_id="t2")] + mock_fetch.return_value = mock_threads + mock_process.return_value = (2, 0, []) # succeeded, failed, failed_threads + + _handle_bulk_resolve(ctx, 123, False, True, False, 100) + + mock_fetch.assert_called_once_with(123, False, False, 100) + mock_confirm.assert_called_once_with( + ctx, mock_threads, "resolve", "๐Ÿ”’", 123, True, False + ) + mock_process.assert_called_once_with( + mock_threads, False, "Resolving", "๐Ÿ”’", False + ) + mock_display.assert_called_once_with( + mock_threads, 2, 0, [], "resolve", "resolved", 123, False + ) + + @patch("toady.commands.resolve._fetch_and_filter_threads") + @patch("toady.commands.resolve._handle_confirmation_prompt") + @patch("toady.commands.resolve._process_threads") + @patch("toady.commands.resolve._display_summary") + def test_handle_bulk_resolve_with_failures( + self, mock_display, mock_process, mock_confirm, mock_fetch + ): + """Test bulk resolve with some failures.""" + ctx = Mock() + mock_threads = [Mock(thread_id="t1")] + mock_fetch.return_value = mock_threads + mock_process.return_value = (0, 1, [{"thread_id": "t1", "error": "Failed"}]) + + _handle_bulk_resolve(ctx, 123, False, False, False, 100) + + # Should exit with error code when there are failures + ctx.exit.assert_called_once_with(1) + + @patch("toady.commands.resolve._fetch_and_filter_threads") + @patch("toady.commands.resolve._handle_bulk_resolve_error") + def test_handle_bulk_resolve_exception_handling( + self, mock_handle_error, mock_fetch + ): + """Test bulk resolve exception handling.""" + ctx = Mock() + mock_error = FetchServiceError("Fetch failed") + mock_fetch.side_effect = mock_error + + _handle_bulk_resolve(ctx, 123, False, False, False, 100) + + mock_handle_error.assert_called_once_with( + ctx, mock_error, 123, "resolve", False + ) + + +class TestResolveCommandIntegration: + """Test resolve command integration with CLI.""" + + @patch("toady.commands.resolve.resolve_format_from_options") + @patch("toady.commands.resolve._validate_resolve_parameters") + @patch("toady.commands.resolve._handle_single_resolve") + def test_resolve_command_single_thread_flow( + self, mock_handle_single, mock_validate, mock_resolve_format, runner + ): + """Test resolve command single thread flow.""" + mock_resolve_format.return_value = "json" + + runner.invoke(cli, ["resolve", "--thread-id", "thread123"]) + + mock_resolve_format.assert_called_once_with(None, False) + mock_validate.assert_called_once_with(False, "thread123", None, 100) + mock_handle_single.assert_called_once() + + @patch("toady.commands.resolve.resolve_format_from_options") + @patch("toady.commands.resolve._validate_resolve_parameters") + @patch("toady.commands.resolve._handle_bulk_resolve") + def test_resolve_command_bulk_flow( + self, mock_handle_bulk, mock_validate, mock_resolve_format, runner + ): + """Test resolve command bulk flow.""" + mock_resolve_format.return_value = "pretty" + + runner.invoke(cli, ["resolve", "--all", "--pr", "123", "--pretty"]) + + mock_resolve_format.assert_called_once_with(None, True) + mock_validate.assert_called_once_with(True, None, 123, 100) + mock_handle_bulk.assert_called_once() + + def test_resolve_command_validation_error(self, runner): + """Test resolve command validation error handling.""" + result = runner.invoke(cli, ["resolve"]) + assert result.exit_code != 0 + assert "Must specify either --thread-id or --all" in result.output + + @patch("toady.commands.resolve.resolve_format_from_options") + def test_resolve_command_format_error(self, mock_resolve_format, runner): + """Test resolve command format resolution error.""" + mock_resolve_format.side_effect = ValueError("Format error") + + result = runner.invoke(cli, ["resolve", "--thread-id", "thread123"]) + assert result.exit_code == 1 + assert "Error: Format error" in result.output + + +class TestResolveCommandParameterCombinations: + """Test various parameter combinations in the resolve command.""" + + @patch("toady.commands.resolve.resolve_format_from_options") + @patch("toady.commands.resolve._validate_resolve_parameters") + @patch("toady.commands.resolve._handle_single_resolve") + def test_all_single_parameters_combination( + self, mock_handle_single, mock_validate, mock_resolve_format, runner + ): + """Test resolve command with all single thread parameters.""" + mock_resolve_format.return_value = "pretty" + + runner.invoke( + cli, + [ + "resolve", + "--thread-id", + "thread123", + "--undo", + "--format", + "pretty", + "--pretty", + ], + ) + + mock_resolve_format.assert_called_once_with("pretty", True) + mock_validate.assert_called_once_with(False, "thread123", None, 100) + + @patch("toady.commands.resolve.resolve_format_from_options") + @patch("toady.commands.resolve._validate_resolve_parameters") + @patch("toady.commands.resolve._handle_bulk_resolve") + def test_all_bulk_parameters_combination( + self, mock_handle_bulk, mock_validate, mock_resolve_format, runner + ): + """Test resolve command with all bulk parameters.""" + mock_resolve_format.return_value = "json" + + runner.invoke( + cli, + [ + "resolve", + "--all", + "--pr", + "456", + "--undo", + "--yes", + "--limit", + "200", + ], + ) + + mock_resolve_format.assert_called_once_with(None, False) + mock_validate.assert_called_once_with(True, None, 456, 200) + + def test_boundary_limit_values(self, runner): + """Test resolve command with boundary limit values.""" + # Test with limit 1 + result = runner.invoke(cli, ["resolve", "--all", "--pr", "123", "--limit", "1"]) + # Should not fail on limit validation + + # Test with limit 1000 (max) + result = runner.invoke( + cli, ["resolve", "--all", "--pr", "123", "--limit", "1000"] + ) + # Should not fail on limit validation + + # Test with limit over 1000 + result = runner.invoke( + cli, ["resolve", "--all", "--pr", "123", "--limit", "1001"] + ) + assert result.exit_code != 0 + assert "Limit cannot exceed 1000" in result.output + + +@pytest.fixture(scope="module") +def runner(): + """Create a Click CLI test runner for the module.""" + return CliRunner() diff --git a/tests/unit/commands/test_schema.py b/tests/unit/commands/test_schema.py new file mode 100644 index 0000000..8e62224 --- /dev/null +++ b/tests/unit/commands/test_schema.py @@ -0,0 +1,1238 @@ +"""Unit tests for the schema command module. + +This module tests the core schema command logic, including parameter validation, +error handling, format resolution, and validator integration. It focuses on unit +testing the command implementation without testing the CLI interface directly. +""" + +from unittest.mock import MagicMock, Mock, patch + +import click +import pytest + +from toady.cli import cli +from toady.commands.schema import ( + _display_query_validation_results, + _display_summary_report, + _has_critical_errors, + check, + fetch, + schema, + validate, +) +from toady.exceptions import ( + ToadyError, +) +from toady.validators.schema_validator import ( + SchemaValidationError, +) + + +class TestSchemaCommandCore: + """Test the core schema command functionality.""" + + def test_schema_command_exists(self): + """Test that the schema command is properly defined.""" + assert schema is not None + assert callable(schema) + assert hasattr(schema, "commands") + + def test_schema_command_is_group(self): + """Test that schema is a Click command group.""" + assert isinstance(schema, click.Group) + assert schema.invoke_without_command is True + + def test_schema_subcommands_exist(self): + """Test that all expected subcommands exist.""" + expected_commands = ["validate", "fetch", "check"] + for cmd_name in expected_commands: + assert cmd_name in schema.commands + assert callable(schema.commands[cmd_name]) + + def test_validate_command_parameters(self): + """Test that validate command has expected parameters.""" + param_names = [param.name for param in validate.params] + expected_params = ["cache_dir", "force_refresh", "output"] + + for expected_param in expected_params: + assert expected_param in param_names, f"Missing parameter: {expected_param}" + + def test_fetch_command_parameters(self): + """Test that fetch command has expected parameters.""" + param_names = [param.name for param in fetch.params] + expected_params = ["cache_dir", "force_refresh"] + + for expected_param in expected_params: + assert expected_param in param_names, f"Missing parameter: {expected_param}" + + def test_check_command_parameters(self): + """Test that check command has expected parameters.""" + param_names = [param.name for param in check.params] + expected_params = ["query", "cache_dir", "output"] + + for expected_param in expected_params: + assert expected_param in param_names, f"Missing parameter: {expected_param}" + + def test_validate_command_defaults(self): + """Test validate command parameter defaults.""" + param_defaults = {param.name: param.default for param in validate.params} + + assert param_defaults["force_refresh"] is False + assert param_defaults["output"] == "summary" + assert param_defaults["cache_dir"] is None + + def test_check_command_defaults(self): + """Test check command parameter defaults.""" + param_defaults = {param.name: param.default for param in check.params} + + assert param_defaults["output"] == "summary" + assert param_defaults["cache_dir"] is None + + +class TestValidateCommand: + """Test the validate subcommand functionality.""" + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_success_summary_format(self, mock_validator_class, runner): + """Test successful validation with summary output format.""" + # Setup mock validator + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {}, + "mutations": {}, + "recommendations": [], + } + mock_validator.generate_compatibility_report.return_value = mock_report + mock_validator_class.return_value = mock_validator + + # Test direct command invocation + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir=None) + mock_validator.fetch_schema.assert_called_once_with(force_refresh=False) + mock_validator.generate_compatibility_report.assert_called_once() + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_success_json_format(self, mock_validator_class, runner): + """Test successful validation with JSON output format.""" + # Setup mock validator + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {}, + "mutations": {}, + "recommendations": [], + } + mock_validator.generate_compatibility_report.return_value = mock_report + mock_validator_class.return_value = mock_validator + + # Test direct command invocation + result = runner.invoke(cli, ["schema", "validate", "--output", "json"]) + + assert result.exit_code == 0 + # Check that JSON output contains expected fields + assert '"timestamp": "2024-01-15T10:00:00Z"' in result.output + assert '"schema_version": "v1.0"' in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_with_custom_cache_dir(self, mock_validator_class, runner): + """Test validation with custom cache directory.""" + # Setup mock validator + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_validator.generate_compatibility_report.return_value = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {}, + "mutations": {}, + "recommendations": [], + } + mock_validator_class.return_value = mock_validator + + # Test with custom cache directory + result = runner.invoke( + cli, ["schema", "validate", "--cache-dir", "/tmp/test-cache"] + ) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir="/tmp/test-cache") + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_with_force_refresh(self, mock_validator_class, runner): + """Test validation with force refresh flag.""" + # Setup mock validator + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_validator.generate_compatibility_report.return_value = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {}, + "mutations": {}, + "recommendations": [], + } + mock_validator_class.return_value = mock_validator + + # Test with force refresh + result = runner.invoke(cli, ["schema", "validate", "--force-refresh"]) + + assert result.exit_code == 0 + mock_validator.fetch_schema.assert_called_once_with(force_refresh=True) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_validator_initialization_os_error( + self, mock_validator_class, runner + ): + """Test validation when validator initialization fails with OSError.""" + mock_validator_class.side_effect = OSError("Permission denied") + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Error: Failed to initialize schema validator" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_validator_initialization_permission_error( + self, mock_validator_class, runner + ): + """Test validation when validator initialization fails with PermissionError.""" + mock_validator_class.side_effect = PermissionError("Access denied") + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Error: Failed to initialize schema validator" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_validator_initialization_generic_error( + self, mock_validator_class, runner + ): + """Test validation when validator initialization fails with generic error.""" + mock_validator_class.side_effect = RuntimeError("Unexpected error") + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert ( + "Error: Configuration error initializing schema validator" in result.output + ) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_schema_fetch_connection_error(self, mock_validator_class, runner): + """Test validation when schema fetch fails with connection error.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = ConnectionError("Network timeout") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Error: Network error fetching GitHub schema" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_schema_fetch_timeout_error(self, mock_validator_class, runner): + """Test validation when schema fetch fails with timeout error.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = TimeoutError("Request timeout") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Error: Network error fetching GitHub schema" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_schema_fetch_file_operation_error( + self, mock_validator_class, runner + ): + """Test validation when schema fetch fails with file operation error.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = OSError("Disk full") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Error: File operation error during schema fetch" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_report_generation_error(self, mock_validator_class, runner): + """Test validation when report generation fails.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_validator.generate_compatibility_report.side_effect = Exception( + "Report error" + ) + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Failed to generate compatibility report" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_json_output_format_error(self, mock_validator_class, runner): + """Test validation when JSON output formatting fails.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + # Return non-serializable object + mock_validator.generate_compatibility_report.return_value = {"timestamp": set()} + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate", "--output", "json"]) + + assert result.exit_code == 1 + assert "Failed to format output" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_summary_display_error(self, mock_validator_class, runner): + """Test validation when summary display fails.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + # Return invalid report format + mock_validator.generate_compatibility_report.return_value = "invalid_report" + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Invalid report format for display" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_critical_error_analysis_failure( + self, mock_validator_class, runner + ): + """Test validation when critical error analysis fails.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + # Return invalid report that will cause error analysis to fail + mock_validator.generate_compatibility_report.return_value = "invalid_report" + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate", "--output", "json"]) + + assert result.exit_code == 1 + assert "Failed to analyze report for critical errors" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_with_critical_errors_exits_one( + self, mock_validator_class, runner + ): + """Test validation exits with code 1 when critical errors exist.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": { + "test_query": [{"message": "Critical error", "severity": "error"}] + }, + "mutations": {}, + "recommendations": [], + } + mock_validator.generate_compatibility_report.return_value = mock_report + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_with_warnings_only_exits_zero(self, mock_validator_class, runner): + """Test validation exits with code 0 when only warnings exist.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {"test_query": [{"message": "Warning", "severity": "warning"}]}, + "mutations": {}, + "recommendations": [], + } + mock_validator.generate_compatibility_report.return_value = mock_report + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 0 + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_schema_validation_error_during_init( + self, mock_validator_class, runner + ): + """Test validation when SchemaValidationError is raised during init.""" + mock_validator_class.side_effect = SchemaValidationError( + "Schema invalid", suggestions=["Fix syntax"] + ) + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert ( + "Error: Configuration error initializing schema validator" in result.output + ) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_schema_validation_error_from_fetch( + self, mock_validator_class, runner + ): + """Test validation when SchemaValidationError is raised during fetch.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = SchemaValidationError( + "Schema fetch error", suggestions=["Check connection"] + ) + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Schema validation failed: Schema fetch error" in result.output + assert "Suggestions:" in result.output + assert "Check connection" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_schema_validation_error_no_suggestions( + self, mock_validator_class, runner + ): + """Test validation when SchemaValidationError has no suggestions.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = SchemaValidationError("Schema error") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Schema validation failed: Schema error" in result.output + assert "Suggestions:" not in result.output + + +class TestFetchCommand: + """Test the fetch subcommand functionality.""" + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_success(self, mock_validator_class, runner): + """Test successful schema fetch.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = {"types": [{"name": "Query"}]} + mock_validator.get_schema_version.return_value = "v1.0" + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir=None) + mock_validator.fetch_schema.assert_called_once_with(force_refresh=False) + mock_validator.get_schema_version.assert_called_once() + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_with_custom_cache_dir(self, mock_validator_class, runner): + """Test fetch with custom cache directory.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = {"types": []} + mock_validator.get_schema_version.return_value = "v1.0" + mock_validator_class.return_value = mock_validator + + result = runner.invoke( + cli, ["schema", "fetch", "--cache-dir", "/tmp/test-cache"] + ) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir="/tmp/test-cache") + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_with_force_refresh(self, mock_validator_class, runner): + """Test fetch with force refresh flag.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = {"types": []} + mock_validator.get_schema_version.return_value = "v1.0" + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch", "--force-refresh"]) + + assert result.exit_code == 0 + mock_validator.fetch_schema.assert_called_once_with(force_refresh=True) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_validator_initialization_error(self, mock_validator_class, runner): + """Test fetch when validator initialization fails.""" + mock_validator_class.side_effect = OSError("Permission denied") + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + assert "Error: Failed to initialize schema validator" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_network_error(self, mock_validator_class, runner): + """Test fetch when network error occurs.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = ConnectionError("Network timeout") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + assert "Error: Network error fetching GitHub schema" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_file_operation_error(self, mock_validator_class, runner): + """Test fetch when file operation error occurs.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = PermissionError("Access denied") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + assert "Error: File operation error during schema fetch" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_version_retrieval_error(self, mock_validator_class, runner): + """Test fetch when version retrieval fails.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = {"types": []} + mock_validator.get_schema_version.side_effect = Exception("Version error") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 0 + assert "version: unknown" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_schema_analysis_error(self, mock_validator_class, runner): + """Test fetch when schema analysis fails.""" + mock_validator = Mock() + # Create mock schema where len() will fail + mock_types = MagicMock() + mock_types.__len__.side_effect = Exception("Length error") + mock_schema = {"types": mock_types} + mock_validator.fetch_schema.return_value = mock_schema + mock_validator.get_schema_version.return_value = "v1.0" + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 0 + assert "unable to analyze structure" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_schema_validation_error_during_init( + self, mock_validator_class, runner + ): + """Test fetch when SchemaValidationError is raised during initialization.""" + mock_validator_class.side_effect = SchemaValidationError("Schema invalid") + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + assert ( + "Error: Configuration error initializing schema validator" in result.output + ) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_schema_validation_error_from_fetch( + self, mock_validator_class, runner + ): + """Test fetch when SchemaValidationError is raised during fetch operation.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = SchemaValidationError( + "Schema fetch error" + ) + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + assert "Failed to fetch schema: Schema fetch error" in result.output + + +class TestCheckCommand: + """Test the check subcommand functionality.""" + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_success(self, mock_validator_class, runner): + """Test successful query validation.""" + mock_validator = Mock() + mock_validator.validate_query.return_value = [] + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir=None) + mock_validator.validate_query.assert_called_once_with( + "query { viewer { login } }" + ) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_with_errors(self, mock_validator_class, runner): + """Test query validation with errors.""" + mock_validator = Mock() + mock_validator.validate_query.return_value = [ + {"message": "Field not found", "severity": "error"} + ] + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { invalid }"]) + + assert result.exit_code == 1 + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_with_warnings_only(self, mock_validator_class, runner): + """Test query validation with warnings only.""" + mock_validator = Mock() + mock_validator.validate_query.return_value = [ + {"message": "Deprecated field", "severity": "warning"} + ] + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { deprecated }"]) + + assert result.exit_code == 0 + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_json_output(self, mock_validator_class, runner): + """Test query validation with JSON output.""" + mock_validator = Mock() + errors = [{"message": "Field not found", "severity": "error"}] + mock_validator.validate_query.return_value = errors + mock_validator_class.return_value = mock_validator + + result = runner.invoke( + cli, ["schema", "check", "query { invalid }", "--output", "json"] + ) + + assert result.exit_code == 1 + assert '"errors":' in result.output + assert '"message": "Field not found"' in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_with_custom_cache_dir(self, mock_validator_class, runner): + """Test query validation with custom cache directory.""" + mock_validator = Mock() + mock_validator.validate_query.return_value = [] + mock_validator_class.return_value = mock_validator + + result = runner.invoke( + cli, + [ + "schema", + "check", + "query { viewer { login } }", + "--cache-dir", + "/tmp/test", + ], + ) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir="/tmp/test") + + def test_check_empty_query(self, runner): + """Test query validation with empty query.""" + result = runner.invoke(cli, ["schema", "check", ""]) + + assert result.exit_code == 1 + assert "Query must be a non-empty string" in result.output + + def test_check_whitespace_only_query(self, runner): + """Test query validation with whitespace-only query.""" + result = runner.invoke(cli, ["schema", "check", " \n\t "]) + + assert result.exit_code == 1 + assert "Query must be a non-empty string" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_validator_initialization_error(self, mock_validator_class, runner): + """Test check when validator initialization fails.""" + mock_validator_class.side_effect = OSError("Permission denied") + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 1 + assert "Error: Failed to initialize schema validator" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_validation_error(self, mock_validator_class, runner): + """Test check when query validation fails internally.""" + mock_validator = Mock() + mock_validator.validate_query.side_effect = Exception( + "Internal validation error" + ) + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 1 + assert "Failed to validate query" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_results_formatting_error(self, mock_validator_class, runner): + """Test check when results formatting fails.""" + mock_validator = Mock() + # Return invalid object that will cause formatting error + mock_validator.validate_query.return_value = [{"message": set()}] + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query {}", "--output", "json"]) + + assert result.exit_code == 1 + assert "Failed to format validation results" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_error_analysis_failure(self, mock_validator_class, runner): + """Test check when error analysis fails.""" + mock_validator = Mock() + # Return non-list errors that will cause analysis to fail + mock_validator.validate_query.return_value = "invalid_errors_format" + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query {}"]) + + assert result.exit_code == 1 + assert "Failed to process validation results" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_schema_validation_error_during_init( + self, mock_validator_class, runner + ): + """Test check when SchemaValidationError is raised during initialization.""" + mock_validator_class.side_effect = SchemaValidationError("Schema invalid") + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 1 + assert ( + "Error: Configuration error initializing schema validator" in result.output + ) + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_schema_validation_error_from_validate( + self, mock_validator_class, runner + ): + """Test check when SchemaValidationError is raised during validation.""" + mock_validator = Mock() + mock_validator.validate_query.side_effect = SchemaValidationError( + "Validation error" + ) + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 1 + assert "Failed to validate query: Validation error" in result.output + + +class TestDisplaySummaryReport: + """Test the _display_summary_report function.""" + + def test_display_summary_valid_report(self, capsys): + """Test displaying a valid summary report.""" + report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {"test_query": [{"message": "Error", "severity": "error"}]}, + "mutations": {}, + "recommendations": ["Use latest API version"], + } + + _display_summary_report(report) + + captured = capsys.readouterr() + assert "GitHub GraphQL Schema Compatibility Report" in captured.out + assert "Generated: 2024-01-15T10:00:00Z" in captured.out + assert "Schema Version: v1.0" in captured.out + assert "test_query: 1 error(s)" in captured.out + assert "Use latest API version" in captured.out + + def test_display_summary_empty_report(self, capsys): + """Test displaying an empty summary report.""" + report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {}, + "mutations": {}, + "recommendations": [], + } + + _display_summary_report(report) + + captured = capsys.readouterr() + assert "All queries are valid" in captured.out + assert "All mutations are valid" in captured.out + + def test_display_summary_invalid_report_type(self): + """Test displaying summary with invalid report type.""" + with pytest.raises(ToadyError) as exc_info: + _display_summary_report("invalid_report") + + assert "Invalid report format for display" in str(exc_info.value) + + def test_display_summary_missing_fields(self, capsys): + """Test displaying summary with missing fields.""" + report = {} + + _display_summary_report(report) + + captured = capsys.readouterr() + assert "Generated: Unknown" in captured.out + assert "Schema Version: Unknown" in captured.out + + def test_display_summary_with_warnings(self, capsys): + """Test displaying summary with warnings and errors.""" + report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": { + "query1": [ + {"message": "Error", "severity": "error"}, + {"message": "Warning", "severity": "warning"}, + ], + "query2": [{"message": "Warning only", "severity": "warning"}], + }, + "mutations": { + "mutation1": [{"message": "Mutation error", "severity": "error"}] + }, + "recommendations": [], + } + + _display_summary_report(report) + + captured = capsys.readouterr() + assert "query1: 1 error(s)" in captured.out + assert "query2: 1 warning(s)" in captured.out + assert "mutation1: 1 error(s)" in captured.out + + +class TestDisplayQueryValidationResults: + """Test the _display_query_validation_results function.""" + + def test_display_no_errors(self, capsys): + """Test displaying results with no errors.""" + errors = [] + + _display_query_validation_results(errors) + + captured = capsys.readouterr() + assert "โœ“ Query is valid" in captured.out + + def test_display_critical_errors(self, capsys): + """Test displaying results with critical errors.""" + errors = [ + { + "message": "Field not found", + "severity": "error", + "path": "query.field", + "suggestions": ["Use correct field name"], + } + ] + + _display_query_validation_results(errors) + + captured = capsys.readouterr() + assert "โœ— Query has 1 error(s):" in captured.out + assert "Field not found" in captured.out + assert "Path: query.field" in captured.out + assert "Suggestions: Use correct field name" in captured.out + + def test_display_warnings(self, capsys): + """Test displaying results with warnings.""" + errors = [ + { + "message": "Deprecated field", + "severity": "warning", + "path": "query.deprecatedField", + } + ] + + _display_query_validation_results(errors) + + captured = capsys.readouterr() + assert "โš  Query has 1 warning(s):" in captured.out + assert "Deprecated field" in captured.out + + def test_display_mixed_errors_and_warnings(self, capsys): + """Test displaying results with both errors and warnings.""" + errors = [ + {"message": "Critical error", "severity": "error"}, + {"message": "Warning message", "severity": "warning"}, + ] + + _display_query_validation_results(errors) + + captured = capsys.readouterr() + assert "โœ— Query has 1 error(s):" in captured.out + assert "โš  Query has 1 warning(s):" in captured.out + + def test_display_invalid_errors_type(self): + """Test displaying results with invalid errors type.""" + with pytest.raises(ToadyError) as exc_info: + _display_query_validation_results("invalid_errors") + + assert "Invalid errors format for display" in str(exc_info.value) + + def test_display_errors_with_invalid_error_entries(self, capsys): + """Test displaying results with some invalid error entries.""" + errors = [ + {"message": "Valid error", "severity": "error"}, + "invalid_error_entry", # This should be skipped + {"message": "Another valid error", "severity": "warning"}, + ] + + _display_query_validation_results(errors) + + captured = capsys.readouterr() + assert "โœ— Query has 1 error(s):" in captured.out + assert "โš  Query has 1 warning(s):" in captured.out + assert "Valid error" in captured.out + assert "Another valid error" in captured.out + + def test_display_errors_without_severity(self, capsys): + """Test displaying results with errors without severity. + + Defaults to critical. + """ + errors = [{"message": "Error without severity"}] + + _display_query_validation_results(errors) + + captured = capsys.readouterr() + assert "โœ— Query has 1 error(s):" in captured.out + assert "Error without severity" in captured.out + + +class TestHasCriticalErrors: + """Test the _has_critical_errors function.""" + + def test_has_critical_errors_with_query_errors(self): + """Test detecting critical errors in queries.""" + report = { + "queries": {"test_query": [{"message": "Error", "severity": "error"}]}, + "mutations": {}, + } + + assert _has_critical_errors(report) is True + + def test_has_critical_errors_with_mutation_errors(self): + """Test detecting critical errors in mutations.""" + report = { + "queries": {}, + "mutations": {"test_mutation": [{"message": "Error", "severity": "error"}]}, + } + + assert _has_critical_errors(report) is True + + def test_has_critical_errors_with_warnings_only(self): + """Test no critical errors when only warnings exist.""" + report = { + "queries": {"test_query": [{"message": "Warning", "severity": "warning"}]}, + "mutations": { + "test_mutation": [{"message": "Warning", "severity": "warning"}] + }, + } + + assert _has_critical_errors(report) is False + + def test_has_critical_errors_no_errors(self): + """Test no critical errors when no errors exist.""" + report = {"queries": {}, "mutations": {}} + + assert _has_critical_errors(report) is False + + def test_has_critical_errors_missing_severity(self): + """Test treating missing severity as critical error.""" + report = { + "queries": {"test_query": [{"message": "Error without severity"}]}, + "mutations": {}, + } + + assert _has_critical_errors(report) is True + + def test_has_critical_errors_invalid_report_type(self): + """Test error analysis with invalid report type.""" + with pytest.raises(ToadyError) as exc_info: + _has_critical_errors("invalid_report") + + assert "Invalid report format for error analysis" in str(exc_info.value) + + def test_has_critical_errors_invalid_queries_type(self): + """Test error analysis with invalid queries type.""" + report = {"queries": "invalid_queries_type", "mutations": {}} + + assert _has_critical_errors(report) is False + + def test_has_critical_errors_invalid_error_list(self): + """Test error analysis with invalid error list.""" + report = {"queries": {"test_query": "invalid_error_list"}, "mutations": {}} + + assert _has_critical_errors(report) is False + + def test_has_critical_errors_invalid_error_dict(self): + """Test error analysis with invalid error dictionary.""" + report = {"queries": {"test_query": ["invalid_error_dict"]}, "mutations": {}} + + assert _has_critical_errors(report) is False + + +class TestSchemaParameterValidation: + """Test parameter validation for schema commands.""" + + def test_validate_invalid_cache_dir_type(self, runner): + """Test validation with invalid cache directory type.""" + # Click handles basic type validation, but our command validates further + # Test with a path that our validation logic can handle + result = runner.invoke(cli, ["schema", "validate", "--cache-dir", "/tmp"]) + # This should work as it's a valid path string + assert result.exit_code in [0, 1] # May succeed or fail depending on mock setup + + def test_check_query_type_validation(self, runner): + """Test check command with proper query type validation.""" + # Our validation logic checks for string type and non-empty content + result = runner.invoke(cli, ["schema", "check", ""]) + assert result.exit_code == 1 + assert "Query must be a non-empty string" in result.output + + +class TestSchemaGroupCommand: + """Test the schema group command behavior.""" + + def test_schema_group_without_subcommand(self, runner): + """Test schema group command without subcommand shows help.""" + result = runner.invoke(cli, ["schema"]) + + assert result.exit_code == 0 + assert "Usage:" in result.output + assert "validate" in result.output + assert "fetch" in result.output + assert "check" in result.output + + def test_schema_group_with_help_flag(self, runner): + """Test schema group command with help flag.""" + result = runner.invoke(cli, ["schema", "--help"]) + + assert result.exit_code == 0 + assert "Schema validation commands" in result.output + + +class TestSchemaErrorHandling: + """Test comprehensive error handling for schema commands.""" + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_unexpected_exception_during_execution( + self, mock_validator_class, runner + ): + """Test validate command handling unexpected exceptions during execution.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_validator.generate_compatibility_report.side_effect = RuntimeError( + "Unexpected error" + ) + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Failed to generate compatibility report" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_unexpected_exception_during_execution( + self, mock_validator_class, runner + ): + """Test fetch command handling unexpected exceptions during execution.""" + mock_validator = Mock() + mock_validator.fetch_schema.side_effect = RuntimeError("Unexpected error") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + assert "Unexpected error" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_unexpected_exception_during_execution( + self, mock_validator_class, runner + ): + """Test check command handling unexpected exceptions during execution.""" + mock_validator = Mock() + mock_validator.validate_query.side_effect = RuntimeError("Unexpected error") + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 1 + assert "Failed to validate query" in result.output + + +class TestToadyErrorHandling: + """Test ToadyError exception handling with context and suggestions.""" + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_toady_error_with_suggestions_and_context( + self, mock_validator_class, runner + ): + """Test validation when ToadyError is raised with suggestions and context.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + + # Create ToadyError with suggestions and context + toady_error = ToadyError( + message="Custom validation error", + suggestions=["Try updating the schema", "Check network connection"], + context={"error_code": 500, "url": "https://api.github.com/graphql"}, + ) + mock_validator.generate_compatibility_report.side_effect = toady_error + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "validate"]) + + assert result.exit_code == 1 + assert "Failed to generate compatibility report" in result.output + assert "Custom validation error" in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_toady_error_handled_correctly(self, mock_validator_class, runner): + """Test fetch when ToadyError is raised and handled by outer handler.""" + mock_validator = Mock() + + # Create ToadyError with context + toady_error = ToadyError( + message="Fetch error occurred", + context={"operation": "schema_fetch", "timestamp": "2024-01-15T10:00:00Z"}, + ) + mock_validator.fetch_schema.side_effect = toady_error + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "fetch"]) + + assert result.exit_code == 1 + # ToadyError is handled by the outer handler properly + assert "Error: Fetch error occurred" in result.output + assert "Context:" in result.output + assert '"operation": "schema_fetch"' in result.output + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_toady_error_suggestions_only(self, mock_validator_class, runner): + """Test check when ToadyError is raised with suggestions but no context.""" + mock_validator = Mock() + + # Create ToadyError with suggestions only + toady_error = ToadyError( + message="Query validation failed", + suggestions=["Check GraphQL syntax", "Verify field names"], + ) + mock_validator.validate_query.side_effect = toady_error + mock_validator_class.return_value = mock_validator + + result = runner.invoke(cli, ["schema", "check", "query { viewer { login } }"]) + + assert result.exit_code == 1 + assert "Failed to validate query" in result.output + assert "Query validation failed" in result.output + + +class TestValidationParameterEdgeCases: + """Test parameter validation edge cases.""" + + def test_validate_cache_dir_parameter_validation(self, runner): + """Test validate command parameter validation for cache_dir.""" + # The command should handle string paths properly + result = runner.invoke( + cli, ["schema", "validate", "--cache-dir", "/tmp/valid-path"] + ) + # This test mainly ensures our parameter validation doesn't crash + assert result.exit_code in [0, 1] # Will depend on mock setup + + def test_fetch_cache_dir_parameter_validation(self, runner): + """Test fetch command parameter validation for cache_dir.""" + # The command should handle string paths properly + result = runner.invoke( + cli, ["schema", "fetch", "--cache-dir", "/tmp/valid-path"] + ) + # This test mainly ensures our parameter validation doesn't crash + assert result.exit_code in [0, 1] # Will depend on mock setup + + def test_check_cache_dir_parameter_validation(self, runner): + """Test check command parameter validation for cache_dir.""" + # The command should handle string paths properly + result = runner.invoke( + cli, + [ + "schema", + "check", + "query { viewer { login } }", + "--cache-dir", + "/tmp/valid-path", + ], + ) + # This test mainly ensures our parameter validation doesn't crash + assert result.exit_code in [0, 1] # Will depend on mock setup + + +class TestSchemaCommandIntegration: + """Test integration aspects of schema commands.""" + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_validate_complete_successful_flow(self, mock_validator_class, runner): + """Test complete successful validation flow.""" + mock_validator = Mock() + mock_validator.fetch_schema.return_value = None + mock_report = { + "timestamp": "2024-01-15T10:00:00Z", + "schema_version": "v1.0", + "queries": {}, + "mutations": {}, + "recommendations": ["Consider upgrading to latest API"], + } + mock_validator.generate_compatibility_report.return_value = mock_report + mock_validator_class.return_value = mock_validator + + result = runner.invoke( + cli, ["schema", "validate", "--force-refresh", "--cache-dir", "/tmp/cache"] + ) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir="/tmp/cache") + mock_validator.fetch_schema.assert_called_once_with(force_refresh=True) + mock_validator.generate_compatibility_report.assert_called_once() + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_fetch_complete_successful_flow(self, mock_validator_class, runner): + """Test complete successful fetch flow.""" + mock_validator = Mock() + mock_schema = {"types": [{"name": "Query"}, {"name": "Mutation"}]} + mock_validator.fetch_schema.return_value = mock_schema + mock_validator.get_schema_version.return_value = "v2.0" + mock_validator_class.return_value = mock_validator + + result = runner.invoke( + cli, ["schema", "fetch", "--force-refresh", "--cache-dir", "/tmp/cache"] + ) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir="/tmp/cache") + mock_validator.fetch_schema.assert_called_once_with(force_refresh=True) + mock_validator.get_schema_version.assert_called_once() + + @patch("toady.commands.schema.GitHubSchemaValidator") + def test_check_complete_successful_flow(self, mock_validator_class, runner): + """Test complete successful check flow.""" + mock_validator = Mock() + mock_validator.validate_query.return_value = [ + {"message": "Warning", "severity": "warning", "path": "query.field"} + ] + mock_validator_class.return_value = mock_validator + + query = "query { viewer { login } }" + result = runner.invoke( + cli, + ["schema", "check", query, "--cache-dir", "/tmp/cache", "--output", "json"], + ) + + assert result.exit_code == 0 + mock_validator_class.assert_called_once_with(cache_dir="/tmp/cache") + mock_validator.validate_query.assert_called_once_with(query) diff --git a/tests/unit/formatters/test_color_and_terminal.py b/tests/unit/formatters/test_color_and_terminal.py index 61c0ed0..2731096 100644 --- a/tests/unit/formatters/test_color_and_terminal.py +++ b/tests/unit/formatters/test_color_and_terminal.py @@ -4,9 +4,9 @@ ANSI escape code handling, and terminal-specific edge cases. """ +from datetime import datetime import os import time -from datetime import datetime from unittest.mock import patch import pytest @@ -221,8 +221,7 @@ def test_different_term_variables(self): def test_no_term_variable(self): """Test behavior when TERM variable is not set.""" env_without_term = dict(os.environ) - if "TERM" in env_without_term: - del env_without_term["TERM"] + env_without_term.pop("TERM", None) with patch.dict(os.environ, env_without_term, clear=True): formatter = PrettyFormatter(use_colors=True) diff --git a/tests/unit/formatters/test_comprehensive_formatting.py b/tests/unit/formatters/test_comprehensive_formatting.py index 283c0bd..070fa2a 100644 --- a/tests/unit/formatters/test_comprehensive_formatting.py +++ b/tests/unit/formatters/test_comprehensive_formatting.py @@ -15,11 +15,11 @@ - Table formatting edge cases """ +from datetime import datetime import json import os import sys import time -from datetime import datetime from unittest.mock import patch import pytest diff --git a/tests/unit/formatters/test_format_interfaces.py b/tests/unit/formatters/test_format_interfaces.py index 8a0e01f..8643616 100644 --- a/tests/unit/formatters/test_format_interfaces.py +++ b/tests/unit/formatters/test_format_interfaces.py @@ -1,7 +1,7 @@ """Tests for the formatter interfaces and base classes.""" from datetime import datetime -from typing import Any, Dict, List +from typing import Any from unittest.mock import Mock import pytest @@ -21,9 +21,9 @@ class MockFormatter(BaseFormatter): def __init__(self, **options: Any) -> None: super().__init__(**options) - self.format_calls: List[str] = [] + self.format_calls: list[str] = [] - def format_threads(self, threads: List[ReviewThread]) -> str: + def format_threads(self, threads: list[ReviewThread]) -> str: self.format_calls.append("format_threads") return f"MockFormatter: {len(threads)} threads" @@ -31,7 +31,7 @@ def format_object(self, obj: Any) -> str: self.format_calls.append("format_object") return f"MockFormatter: object {type(obj).__name__}" - def format_array(self, items: List[Any]) -> str: + def format_array(self, items: list[Any]) -> str: self.format_calls.append("format_array") return f"MockFormatter: array of {len(items)} items" @@ -39,7 +39,7 @@ def format_primitive(self, value: Any) -> str: self.format_calls.append("format_primitive") return f"MockFormatter: primitive {value}" - def format_error(self, error: Dict[str, Any]) -> str: + def format_error(self, error: dict[str, Any]) -> str: self.format_calls.append("format_error") return f"MockFormatter: error {error.get('message', 'unknown')}" diff --git a/tests/unit/formatters/test_formatters.py b/tests/unit/formatters/test_formatters.py index cced25b..9ad2273 100644 --- a/tests/unit/formatters/test_formatters.py +++ b/tests/unit/formatters/test_formatters.py @@ -1,9 +1,11 @@ """Tests for the formatters module.""" -import json from datetime import datetime +import json from unittest.mock import patch +import pytest + from toady.formatters.formatters import ( JSONFormatter, OutputFormatter, @@ -13,6 +15,8 @@ from toady.models.models import Comment, ReviewThread +@pytest.mark.formatter +@pytest.mark.unit class TestJSONFormatter: """Test the JSONFormatter class.""" @@ -99,6 +103,8 @@ def test_json_structure_completeness(self) -> None: assert field in thread_data, f"Missing field: {field}" +@pytest.mark.formatter +@pytest.mark.unit class TestPrettyFormatter: """Test the PrettyFormatter class.""" @@ -248,6 +254,8 @@ def test_datetime_formatting(self) -> None: assert "๐Ÿ”„ Updated: 2024-01-15 10:30:00" in result +@pytest.mark.formatter +@pytest.mark.unit class TestOutputFormatter: """Test the OutputFormatter class.""" @@ -290,6 +298,8 @@ def test_format_threads_pretty_mode(self) -> None: assert "๐Ÿ“ ID: RT_456" in result +@pytest.mark.formatter +@pytest.mark.unit class TestFormatFetchOutput: """Test the format_fetch_output function.""" @@ -378,6 +388,8 @@ def test_format_fetch_output_with_threads(self, mock_echo) -> None: assert "๐Ÿ“ Found 1 unresolved threads" in calls[2] +@pytest.mark.formatter +@pytest.mark.unit class TestFormatterIntegration: """Integration tests for formatter functionality.""" @@ -477,3 +489,160 @@ def test_formatter_consistency(self) -> None: assert thread_data["thread_id"] in pretty_result assert thread_data["title"] in pretty_result assert thread_data["author"] in pretty_result + + +@pytest.mark.formatter +@pytest.mark.unit +class TestPrettyFormatterFileContext: + """Test PrettyFormatter file context functionality.""" + + def test_wrap_text_empty_string(self) -> None: + """Test wrap_text with empty string.""" + result = PrettyFormatter._wrap_text("", width=80, indent=" ") + assert result == "" + + def test_format_file_context_no_file_path(self) -> None: + """Test file context formatting when no file path is present.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + # No file_path set + ) + + result = PrettyFormatter._format_file_context(thread) + assert result == "" + + def test_format_file_context_with_file_path_only(self) -> None: + """Test file context with only file path.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/main.py", + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/main.py" in result + + def test_format_file_context_with_line_info(self) -> None: + """Test file context with line information.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/main.py", + line=42, + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/main.py" in result + assert "๐Ÿ“ Line: 42" in result + + def test_format_file_context_with_line_range(self) -> None: + """Test file context with line range.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/main.py", + line=45, + start_line=42, + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/main.py" in result + assert "๐Ÿ“ Lines: 42-45" in result + + def test_format_file_context_with_diff_side_left(self) -> None: + """Test file context with LEFT diff side.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/main.py", + diff_side="LEFT", + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/main.py" in result + assert "โ—€๏ธ Side: LEFT" in result + + def test_format_file_context_with_diff_side_right(self) -> None: + """Test file context with RIGHT diff side.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/main.py", + diff_side="RIGHT", + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/main.py" in result + assert "โ–ถ๏ธ Side: RIGHT" in result + + def test_format_file_context_outdated(self) -> None: + """Test file context with outdated flag.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/main.py", + is_outdated=True, + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/main.py" in result + assert "โš ๏ธ Outdated" in result + + def test_format_file_context_all_attributes(self) -> None: + """Test file context with all attributes present.""" + thread = ReviewThread( + thread_id="RT_123", + title="Test thread", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + file_path="src/components/Button.tsx", + line=67, + start_line=65, + diff_side="RIGHT", + is_outdated=True, + ) + + result = PrettyFormatter._format_file_context(thread) + assert "๐Ÿ“ File: src/components/Button.tsx" in result + assert "๐Ÿ“ Lines: 65-67" in result + assert "โ–ถ๏ธ Side: RIGHT" in result + assert "โš ๏ธ Outdated" in result diff --git a/tests/unit/formatters/test_json_formatter.py b/tests/unit/formatters/test_json_formatter.py index 1eff6c2..86ca308 100644 --- a/tests/unit/formatters/test_json_formatter.py +++ b/tests/unit/formatters/test_json_formatter.py @@ -1,7 +1,7 @@ """Tests for the JSON formatter implementation.""" -import json from datetime import datetime +import json from unittest.mock import Mock import pytest @@ -640,3 +640,101 @@ def to_dict(self): # Should still produce valid JSON - will be a string representation parsed = json.loads(result) assert isinstance(parsed, str) + + +class TestJSONFormatterSafeSerialize: + """Test the safe serialization fallback paths.""" + + def test_format_threads_without_to_dict(self): + """Test formatting threads that don't have to_dict method.""" + formatter = JSONFormatter() + + # Create a mock thread without to_dict method + class MockThread: + def __init__(self): + self.thread_id = "RT_123" + self.title = "Test thread" + self.status = "UNRESOLVED" + + mock_thread = MockThread() + result = formatter.format_threads([mock_thread]) + parsed = json.loads(result) + + # Should fall back to safe serialization + assert len(parsed) == 1 + # Safe serialization uses __dict__ or string representation + + def test_format_comments_without_to_dict(self): + """Test formatting comments that don't have to_dict method.""" + formatter = JSONFormatter() + + # Create a mock comment without to_dict method + class MockComment: + def __init__(self): + self.comment_id = "C_123" + self.content = "Test comment" + self.author = "testuser" + + mock_comment = MockComment() + result = formatter.format_comments([mock_comment]) + parsed = json.loads(result) + + # Should fall back to safe serialization + assert len(parsed) == 1 + + def test_format_object_without_to_dict(self): + """Test formatting object that doesn't have to_dict method.""" + formatter = JSONFormatter() + + # Create a mock object without to_dict method + class MockObject: + def __init__(self): + self.id = "123" + self.name = "test" + + mock_obj = MockObject() + result = formatter.format_object(mock_obj) + parsed = json.loads(result) + + # Should successfully serialize using safe serialization + assert parsed is not None + + def test_format_threads_serialization_error(self): + """Test formatting when thread serialization fails.""" + formatter = JSONFormatter() + + # Create a thread that will fail during serialization + class FailingThread: + @property + def thread_id(self): + return "RT_123" + + def to_dict(self): + raise RuntimeError("Serialization failed") + + failing_thread = FailingThread() + + with pytest.raises(FormatterError) as exc_info: + formatter.format_threads([failing_thread]) + + assert "Failed to serialize thread RT_123" in str(exc_info.value) + + def test_format_comments_serialization_error(self): + """Test formatting when comment serialization fails.""" + formatter = JSONFormatter() + + # Create a comment that will fail during serialization + class FailingComment: + @property + def comment_id(self): + return "C_123" + + def to_dict(self): + raise RuntimeError("Serialization failed") + + failing_comment = FailingComment() + + with pytest.raises(FormatterError) as exc_info: + formatter.format_comments([failing_comment]) + + assert "Failed to serialize comment C_123" in str(exc_info.value) diff --git a/tests/unit/formatters/test_performance_and_memory.py b/tests/unit/formatters/test_performance_and_memory.py index eed89db..d304274 100644 --- a/tests/unit/formatters/test_performance_and_memory.py +++ b/tests/unit/formatters/test_performance_and_memory.py @@ -4,10 +4,10 @@ to ensure they can handle large datasets efficiently without memory leaks. """ +from datetime import datetime import gc import os import time -from datetime import datetime import pytest diff --git a/tests/unit/models/test_model_edge_cases.py b/tests/unit/models/test_model_edge_cases.py new file mode 100644 index 0000000..d34859d --- /dev/null +++ b/tests/unit/models/test_model_edge_cases.py @@ -0,0 +1,454 @@ +"""Additional edge case tests for model classes to improve coverage.""" + +from datetime import datetime + +import pytest + +from toady.exceptions import ValidationError +from toady.models.models import Comment, PullRequest, ReviewThread, _parse_datetime + + +@pytest.mark.model +@pytest.mark.unit +class TestParseDatetimeWrapper: + """Test the _parse_datetime wrapper function in models module.""" + + def test_parse_datetime_success(self): + """Test successful datetime parsing.""" + result = _parse_datetime("2024-01-15T10:30:45") + expected = datetime(2024, 1, 15, 10, 30, 45) + assert result == expected + + def test_parse_datetime_with_timezone(self): + """Test datetime parsing with timezone.""" + result = _parse_datetime("2024-01-15T10:30:45Z") + expected = datetime(2024, 1, 15, 10, 30, 45) + assert result == expected + + def test_parse_datetime_validation_error_passthrough(self): + """Test that ValidationError is passed through.""" + with pytest.raises(ValidationError) as exc_info: + _parse_datetime("invalid-date") + + error = exc_info.value + assert error.field_name == "date_str" + assert "Unable to parse datetime" in str(error) + + def test_parse_datetime_unexpected_error_wrapping(self): + """Test wrapping of unexpected errors.""" + from unittest.mock import patch + + # Mock parse_datetime to raise a non-ValidationError + with patch( + "toady.models.models.parse_datetime", side_effect=RuntimeError("Unexpected") + ): + with pytest.raises(ValidationError) as exc_info: + _parse_datetime("2024-01-15T10:30:45") + + error = exc_info.value + assert error.field_name == "date_str" + assert "Failed to parse datetime" in str(error) + + +@pytest.mark.model +@pytest.mark.unit +class TestReviewThreadEdgeCasesAdditional: + """Additional edge case tests for ReviewThread.""" + + def test_review_thread_from_dict_with_comment_objects(self): + """Test creating ReviewThread from dict with Comment objects in list.""" + existing_comment = Comment( + comment_id="IC_existing", + content="Existing comment", + author="existing_user", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + parent_id=None, + thread_id="RT_test", + ) + + data = { + "thread_id": "RT_test", + "title": "Test thread", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "status": "UNRESOLVED", + "author": "test_user", + "comments": [existing_comment], # Comment object instead of dict + } + + thread = ReviewThread.from_dict(data) + assert len(thread.comments) == 1 + assert thread.comments[0] is existing_comment + + def test_review_thread_from_dict_mixed_comments(self): + """Test creating ReviewThread with mix of dict and Comment objects.""" + existing_comment = Comment( + comment_id="IC_existing", + content="Existing comment", + author="existing_user", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + parent_id=None, + thread_id="RT_test", + ) + + data = { + "thread_id": "RT_test", + "title": "Test thread", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "status": "UNRESOLVED", + "author": "test_user", + "comments": [ + existing_comment, + { + "comment_id": "IC_new", + "content": "New comment", + "author": "new_user", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "parent_id": None, + "thread_id": "RT_test", + }, + ], + } + + thread = ReviewThread.from_dict(data) + assert len(thread.comments) == 2 + assert thread.comments[0] is existing_comment + assert isinstance(thread.comments[1], Comment) + assert thread.comments[1].comment_id == "IC_new" + + def test_review_thread_all_optional_fields(self): + """Test ReviewThread creation with all optional fields.""" + data = { + "thread_id": "RT_full", + "title": "Full thread", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "status": "RESOLVED", + "author": "full_user", + "comments": [], + "file_path": "src/test.py", + "line": 42, + "original_line": 40, + "start_line": 35, + "original_start_line": 33, + "diff_side": "RIGHT", + "is_outdated": True, + } + + thread = ReviewThread.from_dict(data) + assert thread.file_path == "src/test.py" + assert thread.line == 42 + assert thread.original_line == 40 + assert thread.start_line == 35 + assert thread.original_start_line == 33 + assert thread.diff_side == "RIGHT" + assert thread.is_outdated is True + + def test_review_thread_is_outdated_boolean_conversion(self): + """Test is_outdated field boolean conversion.""" + # Test truthy values + for truthy_value in [True, 1, "true", "yes", [1]]: + data = { + "thread_id": "RT_bool", + "title": "Bool test", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "status": "UNRESOLVED", + "author": "bool_user", + "comments": [], + "is_outdated": truthy_value, + } + thread = ReviewThread.from_dict(data) + assert thread.is_outdated is True + + # Test falsy values + for falsy_value in [False, 0, "", None, []]: + data = { + "thread_id": "RT_bool", + "title": "Bool test", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "status": "UNRESOLVED", + "author": "bool_user", + "comments": [], + "is_outdated": falsy_value, + } + thread = ReviewThread.from_dict(data) + assert thread.is_outdated is False + + +@pytest.mark.model +@pytest.mark.unit +class TestCommentEdgeCasesAdditional: + """Additional edge case tests for Comment.""" + + def test_comment_all_optional_fields(self): + """Test Comment creation with all optional fields.""" + data = { + "comment_id": "IC_full", + "content": "Full comment", + "author": "full_user", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:30:00", + "parent_id": "IC_parent", + "thread_id": "RT_parent", + "author_name": "Full User", + "url": "https://github.com/test/repo/pull/1#issuecomment-123", + "review_id": "PRREV_123", + "review_state": "APPROVED", + } + + comment = Comment.from_dict(data) + assert comment.author_name == "Full User" + assert comment.url == "https://github.com/test/repo/pull/1#issuecomment-123" + assert comment.review_id == "PRREV_123" + assert comment.review_state == "APPROVED" + + def test_comment_minimal_required_fields_only(self): + """Test Comment creation with only required fields.""" + data = { + "comment_id": "IC_minimal", + "content": "Minimal comment", + "author": "minimal_user", + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T10:00:00", + "parent_id": None, + "thread_id": "RT_minimal", + } + + comment = Comment.from_dict(data) + # Verify optional fields have expected defaults + assert comment.author_name is None + assert comment.url is None + assert comment.review_id is None + assert comment.review_state is None + + +@pytest.mark.model +@pytest.mark.unit +class TestPullRequestAdditional: + """Additional tests for PullRequest model.""" + + def test_pull_request_all_fields(self): + """Test PullRequest creation with all fields.""" + pr = PullRequest( + number=123, + title="Test PR", + author="pr_author", + head_ref="feature-branch", + base_ref="main", + is_draft=False, + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 12, 0, 0), + url="https://github.com/test/repo/pull/123", + review_thread_count=5, + node_id="PR_kwDOABcD12MAAAABcDE3fg", + ) + + assert pr.number == 123 + assert pr.title == "Test PR" + assert pr.author == "pr_author" + assert pr.head_ref == "feature-branch" + assert pr.base_ref == "main" + assert pr.is_draft is False + assert pr.url == "https://github.com/test/repo/pull/123" + assert pr.review_thread_count == 5 + assert pr.node_id == "PR_kwDOABcD12MAAAABcDE3fg" + assert pr.created_at == datetime(2024, 1, 15, 10, 0, 0) + assert pr.updated_at == datetime(2024, 1, 15, 12, 0, 0) + + def test_pull_request_from_dict_complete(self): + """Test PullRequest creation from complete dict.""" + data = { + "number": 456, + "title": "Complete PR", + "author": "complete_author", + "head_ref": "feature-456", + "base_ref": "main", + "is_draft": True, + "created_at": "2024-01-15T10:00:00", + "updated_at": "2024-01-15T14:00:00", + "url": "https://github.com/test/repo/pull/456", + "review_thread_count": 3, + "node_id": "PR_kwDOABcD12MAAAABcDE456", + } + + pr = PullRequest.from_dict(data) + assert pr.number == 456 + assert pr.title == "Complete PR" + assert pr.author == "complete_author" + assert pr.head_ref == "feature-456" + assert pr.base_ref == "main" + assert pr.is_draft is True + assert pr.url == "https://github.com/test/repo/pull/456" + assert pr.review_thread_count == 3 + assert pr.node_id == "PR_kwDOABcD12MAAAABcDE456" + assert pr.created_at == datetime(2024, 1, 15, 10, 0, 0) + assert pr.updated_at == datetime(2024, 1, 15, 14, 0, 0) + + def test_pull_request_validation_invalid_number(self): + """Test PullRequest validation with invalid number.""" + with pytest.raises(ValidationError) as exc_info: + PullRequest( + number="not_a_number", + title="Test PR", + author="test_author", + head_ref="feature", + base_ref="main", + is_draft=False, + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + url="https://github.com/test/repo/pull/1", + review_thread_count=0, + ) + + error = exc_info.value + assert error.field_name == "number" + assert "number must be an integer" in str(error) + + def test_pull_request_validation_negative_number(self): + """Test PullRequest validation with negative number.""" + with pytest.raises(ValidationError) as exc_info: + PullRequest( + number=-5, + title="Test PR", + author="test_author", + head_ref="feature", + base_ref="main", + is_draft=False, + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + url="https://github.com/test/repo/pull/1", + review_thread_count=0, + ) + + error = exc_info.value + assert error.field_name == "number" + assert "number must be positive" in str(error) + + def test_pull_request_validation_zero_number(self): + """Test PullRequest validation with zero number.""" + with pytest.raises(ValidationError) as exc_info: + PullRequest( + number=0, + title="Test PR", + author="test_author", + head_ref="feature", + base_ref="main", + is_draft=False, + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + url="https://github.com/test/repo/pull/1", + review_thread_count=0, + ) + + error = exc_info.value + assert error.field_name == "number" + assert "number must be positive" in str(error) + + +@pytest.mark.model +@pytest.mark.unit +class TestModelUtilities: + """Test model utility functions and edge cases.""" + + def test_review_thread_is_resolved_property(self): + """Test ReviewThread.is_resolved property.""" + # Test resolved status + resolved_thread = ReviewThread( + thread_id="RT_resolved", + title="Resolved thread", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + status="RESOLVED", + author="test_user", + comments=[], + ) + assert resolved_thread.is_resolved is True + + # Test unresolved status + unresolved_thread = ReviewThread( + thread_id="RT_unresolved", + title="Unresolved thread", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + status="UNRESOLVED", + author="test_user", + comments=[], + ) + assert unresolved_thread.is_resolved is False + + # Test other statuses + for status in ["PENDING", "OUTDATED", "DISMISSED"]: + thread = ReviewThread( + thread_id=f"RT_{status.lower()}", + title=f"{status} thread", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + status=status, + author="test_user", + comments=[], + ) + assert thread.is_resolved is False + + def test_comment_parent_id_logic(self): + """Test Comment parent_id handling.""" + # Test comment with parent_id (is a reply) + reply_comment = Comment( + comment_id="IC_reply", + content="This is a reply", + author="reply_user", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + parent_id="IC_parent", + thread_id="RT_test", + ) + assert reply_comment.parent_id == "IC_parent" + + # Test comment without parent_id (not a reply) + original_comment = Comment( + comment_id="IC_original", + content="This is an original comment", + author="original_user", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + parent_id=None, + thread_id="RT_test", + ) + assert original_comment.parent_id is None + + def test_comment_content_property_edge_cases(self): + """Test Comment content property with edge cases.""" + # Test with very long content + long_content = "Very long content. " * 1000 # ~19KB content + comment = Comment( + comment_id="IC_long", + content=long_content, + author="long_user", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + parent_id=None, + thread_id="RT_test", + ) + assert comment.content == long_content + assert len(comment.content) > 15000 + + # Test with special characters and unicode + special_content = ( + "Special chars: !@#$%^&*()[]{}|;':\",./<>?`~\nNewline\tTab\r\nCRLF\n๐Ÿš€๐Ÿ’ฏ๐ŸŽ‰" + ) + comment = Comment( + comment_id="IC_special", + content=special_content, + author="special_user", + created_at=datetime(2024, 1, 15, 10, 0, 0), + updated_at=datetime(2024, 1, 15, 10, 0, 0), + parent_id=None, + thread_id="RT_test", + ) + assert comment.content == special_content + assert "๐Ÿš€๐Ÿ’ฏ๐ŸŽ‰" in comment.content diff --git a/tests/unit/models/test_models.py b/tests/unit/models/test_models.py index ff5cdfc..e919e74 100644 --- a/tests/unit/models/test_models.py +++ b/tests/unit/models/test_models.py @@ -1,7 +1,7 @@ """Tests for data models.""" from datetime import datetime -from typing import Any, Dict +from typing import Any import pytest @@ -9,6 +9,8 @@ from toady.models.models import Comment, PullRequest, ReviewThread, _parse_datetime +@pytest.mark.model +@pytest.mark.unit class TestReviewThread: """Test the ReviewThread dataclass.""" @@ -203,7 +205,7 @@ def test_from_dict_valid(self) -> None: def test_from_dict_missing_required_field(self) -> None: """Test deserialization with missing required fields.""" - data: Dict[str, Any] = { + data: dict[str, Any] = { "title": "Test Review", "created_at": "2024-01-01T12:00:00", "updated_at": "2024-01-02T13:00:00", @@ -489,6 +491,8 @@ def test_parse_datetime_unparseable(self) -> None: _parse_datetime("not a date") +@pytest.mark.model +@pytest.mark.unit class TestComment: """Test the Comment dataclass.""" @@ -769,6 +773,8 @@ def test_from_dict_invalid_updated_at_format(self) -> None: Comment.from_dict(data) +@pytest.mark.model +@pytest.mark.unit class TestReviewThreadEdgeCases: """Additional edge case tests for ReviewThread.""" @@ -968,6 +974,8 @@ def test_large_comments_list(self) -> None: assert thread.comments[999].comment_id == "C_999" +@pytest.mark.model +@pytest.mark.unit class TestCommentEdgeCases: """Additional edge case tests for Comment.""" @@ -1125,6 +1133,8 @@ def test_special_characters_in_ids(self) -> None: assert comment.thread_id == special_chars +@pytest.mark.model +@pytest.mark.unit class TestSerializationRoundTrips: """Test serialization and deserialization round-trips.""" @@ -1273,6 +1283,9 @@ def test_multiple_objects_serialization(self) -> None: assert obj.comments[1].comment_id == f"C_{i}_2" +@pytest.mark.model +@pytest.mark.unit +@pytest.mark.slow class TestPerformanceAndLargeData: """Test performance with large datasets.""" @@ -1346,6 +1359,8 @@ def test_large_comments_list_serialization(self) -> None: assert reconstructed.comments[9999].comment_id == "C_9999" +@pytest.mark.model +@pytest.mark.unit class TestPullRequest: """Test the PullRequest dataclass.""" @@ -1721,3 +1736,54 @@ def test_pull_request_roundtrip_serialization(self) -> None: assert reconstructed.url == original.url assert reconstructed.review_thread_count == original.review_thread_count assert reconstructed.node_id == original.node_id + + +@pytest.mark.model +@pytest.mark.unit +class TestTypeValidationEdgeCases: + """Test type validation edge cases for better coverage.""" + + def test_review_thread_invalid_thread_id_type(self) -> None: + """Test ReviewThread with invalid thread_id type.""" + with pytest.raises(ValidationError, match="thread_id must be a string"): + ReviewThread( + thread_id=123, # type: ignore # Invalid: not a string + title="Test", + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + ) + + def test_review_thread_invalid_title_type(self) -> None: + """Test ReviewThread with invalid title type.""" + with pytest.raises(ValidationError, match="title must be a string"): + ReviewThread( + thread_id="RT_123", + title=456, # type: ignore # Invalid: not a string + created_at=datetime.now(), + updated_at=datetime.now(), + status="UNRESOLVED", + author="user", + comments=[], + ) + + def test_review_thread_invalid_status_type(self) -> None: + """Test ReviewThread with invalid status type.""" + with pytest.raises(ValidationError, match="status must be a string"): + ReviewThread( + thread_id="RT_123", + title="Test", + created_at=datetime.now(), + updated_at=datetime.now(), + status=789, # type: ignore # Invalid: not a string + author="user", + comments=[], + ) + + def test_parse_datetime_validation_error_edge_case(self) -> None: + """Test _parse_datetime with edge case exception handling.""" + # Test with an exception that doesn't have error_code attribute + with pytest.raises(ValidationError, match="Unable to parse datetime"): + _parse_datetime("completely-invalid-format") diff --git a/tests/unit/parsers/test_graphql_queries.py b/tests/unit/parsers/test_graphql_queries.py index b04f3ea..d8faca2 100644 --- a/tests/unit/parsers/test_graphql_queries.py +++ b/tests/unit/parsers/test_graphql_queries.py @@ -330,7 +330,8 @@ def test_validate_cursor_with_invalid_characters(self) -> None: def test_validate_cursor_with_empty_string(self) -> None: """Test cursor validation with empty string.""" - assert _validate_cursor("") is False + with pytest.raises(ValueError, match="Cursor cannot be empty"): + _validate_cursor("") def test_validate_cursor_with_invalid_base64(self) -> None: """Test cursor validation with invalid Base64.""" diff --git a/tests/unit/parsers/test_parsers.py b/tests/unit/parsers/test_parsers.py index 5638db9..192c941 100644 --- a/tests/unit/parsers/test_parsers.py +++ b/tests/unit/parsers/test_parsers.py @@ -1,7 +1,7 @@ """Tests for the parsers module.""" from datetime import datetime -from typing import Any, Dict +from typing import Any import pytest @@ -363,7 +363,7 @@ class TestResponseValidator: def test_validate_graphql_response_valid(self) -> None: """Test validation of valid GraphQL response.""" - response: Dict[str, Any] = { + response: dict[str, Any] = { "data": {"repository": {"pullRequest": {"reviewThreads": {"nodes": []}}}} } @@ -377,7 +377,7 @@ def test_validate_graphql_response_not_dict(self) -> None: def test_validate_graphql_response_missing_data(self) -> None: """Test validation fails when data field is missing.""" - response: Dict[str, Any] = {"errors": []} + response: dict[str, Any] = {"errors": []} with pytest.raises(ValidationError) as exc_info: ResponseValidator.validate_graphql_response(response) @@ -767,7 +767,7 @@ class TestResponseValidatorPRs: def test_validate_graphql_prs_response_success(self) -> None: """Test successful validation of PR GraphQL response.""" - response: Dict[str, Any] = { + response: dict[str, Any] = { "data": {"repository": {"pullRequests": {"nodes": []}}} } @@ -775,7 +775,7 @@ def test_validate_graphql_prs_response_success(self) -> None: def test_validate_graphql_prs_response_missing_data(self) -> None: """Test validation fails for missing data field.""" - response: Dict[str, Any] = {"errors": []} + response: dict[str, Any] = {"errors": []} with pytest.raises(ValidationError, match="Response missing 'data' field"): ResponseValidator.validate_graphql_prs_response(response) @@ -824,3 +824,176 @@ def test_validate_pull_request_data_not_dict(self) -> None: ValidationError, match="Pull request data must be a dictionary" ): ResponseValidator.validate_pull_request_data("not a dict") # type: ignore + + +class TestGraphQLResponseParserErrorCases: + """Test error cases for GraphQLResponseParser to improve coverage.""" + + def test_parse_review_threads_nodes_not_list(self) -> None: + """Test parsing when reviewThreads.nodes is not a list.""" + parser = GraphQLResponseParser() + + response = { + "data": { + "repository": { + "pullRequest": { + "reviewThreads": {"nodes": "not a list"} # Should be a list + } + } + } + } + + with pytest.raises(ValidationError, match="reviewThreads.nodes must be a list"): + parser.parse_review_threads_response(response) + + def test_parse_review_threads_thread_parsing_error(self) -> None: + """Test error handling when individual thread parsing fails.""" + parser = GraphQLResponseParser() + + response = { + "data": { + "repository": { + "pullRequest": { + "reviewThreads": { + "nodes": [ + { + # Invalid thread data - missing required fields + "id": "RT_invalid", + # Missing isResolved, comments, etc. + } + ] + } + } + } + } + } + + with pytest.raises(ValidationError, match="Failed to parse thread at index 0"): + parser.parse_review_threads_response(response) + + def test_parse_review_threads_key_error(self) -> None: + """Test handling of KeyError in response parsing.""" + parser = GraphQLResponseParser() + + response = { + "data": { + "repository": { + # Missing pullRequest key + } + } + } + + with pytest.raises( + ValidationError, match="Missing 'pullRequest' in repository data" + ): + parser.parse_review_threads_response(response) + + def test_parse_review_threads_type_error(self) -> None: + """Test handling of TypeError in response parsing.""" + parser = GraphQLResponseParser() + + # Response that will cause TypeError when accessing fields + response = { + "data": { + "repository": { + "pullRequest": None # Will cause AttributeError/TypeError + } + } + } + + with pytest.raises( + ValidationError, match="Pull request not found \\(null value\\)" + ): + parser.parse_review_threads_response(response) + + def test_parse_single_comment_validation_error(self) -> None: + """Test handling validation error in single comment parsing.""" + parser = GraphQLResponseParser() + + # Comment data missing required fields + comment_data = { + # Missing id, body, createdAt, updatedAt + } + + with pytest.raises(ValidationError): + parser._parse_single_comment(comment_data, "RT_test") + + def test_extract_title_empty_comment(self) -> None: + """Test extracting title from empty comment.""" + parser = GraphQLResponseParser() + + title = parser._extract_title_from_comment("") + assert title == "Empty comment" + + def test_extract_title_whitespace_only(self) -> None: + """Test extracting title from whitespace-only comment.""" + parser = GraphQLResponseParser() + + title = parser._extract_title_from_comment(" \n\t ") + assert title == "Empty comment" + + +class TestParserAdditionalCoverage: + """Additional tests to improve parser coverage.""" + + def test_parse_datetime_validation_errors(self) -> None: + """Test datetime parsing validation errors.""" + from toady.utils import parse_datetime + + # Test various invalid datetime formats + invalid_dates = [ + "not-a-date", + "2024-13-45T25:99:99Z", # Invalid date values + "2024/01/15 10:30:00", # Wrong format + "", # Empty string + ] + + for invalid_date in invalid_dates: + with pytest.raises(ValidationError): + parse_datetime(invalid_date) + + def test_parse_comment_missing_required_fields_comprehensive(self) -> None: + """Test comment parsing with comprehensive missing field scenarios.""" + parser = GraphQLResponseParser() + + # Test missing body field + comment_no_body = { + "id": "IC_test", + "createdAt": "2024-01-15T10:30:00Z", + "updatedAt": "2024-01-15T10:30:00Z", + "author": {"login": "testuser"}, + } + + with pytest.raises(ValidationError, match="content cannot be empty"): + parser._parse_single_comment(comment_no_body, "RT_test") + + # Test missing author field - should default to "unknown" + comment_no_author = { + "id": "IC_test", + "body": "Test comment", + "createdAt": "2024-01-15T10:30:00Z", + "updatedAt": "2024-01-15T10:30:00Z", + } + + result = parser._parse_single_comment(comment_no_author, "RT_test") + assert result.author == "unknown" + + def test_response_validation_edge_cases(self) -> None: + """Test additional response validation edge cases.""" + # Test non-dictionary responses + with pytest.raises(ValidationError, match="must be a dictionary"): + ResponseValidator.validate_graphql_response(None) # type: ignore + + with pytest.raises(ValidationError, match="must be a dictionary"): + ResponseValidator.validate_graphql_response([]) # type: ignore + + # Test responses with errors field + response_with_errors = { + "errors": [{"message": "Some GraphQL error"}], + "data": None, + } + + with pytest.raises( + ValidationError, match="Response 'data' field must be a dictionary" + ): + ResponseValidator.validate_graphql_response(response_with_errors) diff --git a/tests/unit/services/test_fetch_service.py b/tests/unit/services/test_fetch_service.py index 8c8c0ba..642705d 100644 --- a/tests/unit/services/test_fetch_service.py +++ b/tests/unit/services/test_fetch_service.py @@ -12,6 +12,8 @@ ) +@pytest.mark.service +@pytest.mark.unit class TestFetchService: """Test the FetchService class.""" @@ -332,6 +334,8 @@ def test_fetch_with_custom_limit(self) -> None: mock_github_service.execute_graphql_query.assert_called_once() +@pytest.mark.service +@pytest.mark.unit class TestFetchServiceExceptions: """Test fetch service exception hierarchy.""" @@ -346,6 +350,8 @@ def test_exception_messages(self) -> None: assert str(exc_info.value) == "Test error" +@pytest.mark.service +@pytest.mark.integration class TestFetchServiceIntegration: """Integration tests for fetch service with edge cases.""" @@ -462,6 +468,8 @@ def test_fetch_handles_missing_optional_fields(self) -> None: assert len(thread.comments) == 1 +@pytest.mark.service +@pytest.mark.unit class TestFetchServicePullRequests: """Test pull request fetching functionality.""" diff --git a/tests/unit/services/test_github_service.py b/tests/unit/services/test_github_service.py index 0fca98a..4946da2 100644 --- a/tests/unit/services/test_github_service.py +++ b/tests/unit/services/test_github_service.py @@ -47,6 +47,37 @@ def test_init_invalid_timeout_non_integer(self) -> None: GitHubService(timeout=30.5) # type: ignore[arg-type] assert "Timeout must be a positive integer" in str(exc_info.value) + def test_service_gh_command_attribute(self) -> None: + """Test GitHubService has gh_command attribute.""" + service = GitHubService() + assert service.gh_command == "gh" + + def test_github_service_error_hierarchy(self) -> None: + """Test exception hierarchy for GitHub service errors.""" + # Test base exception + base_error = GitHubServiceError("Base error") + assert str(base_error) == "Base error" + + # Test API error + api_error = GitHubAPIError("API error") + assert isinstance(api_error, GitHubServiceError) + + # Test authentication error + auth_error = GitHubAuthenticationError("Auth error") + assert isinstance(auth_error, GitHubServiceError) + + # Test CLI not found error + cli_error = GitHubCLINotFoundError("CLI error") + assert isinstance(cli_error, GitHubServiceError) + + # Test rate limit error + rate_error = GitHubRateLimitError("Rate limit error") + assert isinstance(rate_error, GitHubServiceError) + + # Test timeout error + timeout_error = GitHubTimeoutError("Timeout error") + assert isinstance(timeout_error, GitHubServiceError) + @patch("subprocess.run") def test_check_gh_installation_success(self, mock_run: Mock) -> None: """Test successful gh CLI installation check.""" @@ -105,6 +136,54 @@ def test_get_gh_version_failure(self, mock_run: Mock) -> None: with pytest.raises(GitHubCLINotFoundError): service.get_gh_version() + @patch("subprocess.run") + def test_get_gh_version_no_version_line(self, mock_run: Mock) -> None: + """Test version retrieval when no version line is found.""" + mock_run.return_value = Mock( + returncode=0, stdout="Some other output\nwithout version\n" + ) + + service = GitHubService() + version = service.get_gh_version() + + assert version is None + + def test_post_reply_empty_comment_id(self) -> None: + """Test post_reply with empty comment ID.""" + service = GitHubService() + + with pytest.raises(ValueError) as exc_info: + service.post_reply("", "Test body") + + assert "Comment ID cannot be empty" in str(exc_info.value) + + def test_post_reply_empty_body(self) -> None: + """Test post_reply with empty body.""" + service = GitHubService() + + with pytest.raises(ValueError) as exc_info: + service.post_reply("123", "") + + assert "Reply body cannot be empty" in str(exc_info.value) + + def test_post_reply_whitespace_only_comment_id(self) -> None: + """Test post_reply with whitespace-only comment ID.""" + service = GitHubService() + + with pytest.raises(ValueError) as exc_info: + service.post_reply(" ", "Test body") + + assert "Comment ID cannot be empty" in str(exc_info.value) + + def test_post_reply_whitespace_only_body(self) -> None: + """Test post_reply with whitespace-only body.""" + service = GitHubService() + + with pytest.raises(ValueError) as exc_info: + service.post_reply("123", " ") + + assert "Reply body cannot be empty" in str(exc_info.value) + @patch("subprocess.run") def test_check_authentication_success(self, mock_run: Mock) -> None: """Test successful authentication check.""" @@ -546,3 +625,198 @@ def test_exception_messages(self) -> None: with pytest.raises(GitHubRateLimitError) as exc_info: raise GitHubRateLimitError("Rate limit error") assert str(exc_info.value) == "Rate limit error" + + +class TestGitHubServiceEdgeCases: + """Test edge cases and specific error paths in GitHubService.""" + + @patch("subprocess.run") + def test_timeout_error_path(self, mock_run: Mock) -> None: + """Test timeout error detection.""" + import subprocess + + # Mock both the installation check and the actual command + mock_run.side_effect = [ + Mock(returncode=0), # Installation check succeeds + subprocess.TimeoutExpired(["gh", "api", "query"], 30), # Actual command + ] + + service = GitHubService() + with pytest.raises(GitHubTimeoutError) as exc_info: + service.run_gh_command(["api", "query"]) + + assert "timed out after" in str(exc_info.value) + + @patch("subprocess.run") + def test_rate_limit_error_detection(self, mock_run: Mock) -> None: + """Test rate limit error detection in stderr.""" + # Mock both the installation check and the actual command + mock_run.side_effect = [ + Mock(returncode=0), # Installation check succeeds + Mock( + returncode=1, stderr="rate limit exceeded", stdout="" + ), # Actual command + ] + + service = GitHubService() + with pytest.raises(GitHubRateLimitError) as exc_info: + service.run_gh_command(["api", "query"]) + + assert "rate limit exceeded" in str(exc_info.value) + + @patch("subprocess.run") + def test_authentication_error_detection(self, mock_run: Mock) -> None: + """Test authentication error detection in stderr.""" + # Mock both the installation check and the actual command + mock_run.side_effect = [ + Mock(returncode=0), # Installation check succeeds + Mock( + returncode=1, stderr="authentication failed", stdout="" + ), # Actual command + ] + + service = GitHubService() + with pytest.raises(GitHubAuthenticationError) as exc_info: + service.run_gh_command(["api", "query"]) + + assert "authentication failed" in str(exc_info.value) + + @patch.object(GitHubService, "execute_graphql_query") + @patch.object(GitHubService, "_determine_reply_strategy") + def test_post_reply_thread_strategy( + self, mock_strategy: Mock, mock_execute: Mock + ) -> None: + """Test post_reply with thread strategy.""" + mock_strategy.return_value = "thread_reply" + mock_execute.return_value = {"data": {"comment": {"id": "123"}}} + + service = GitHubService() + result = service.post_reply("PRT_kwDOABcD12MAAAABcDE3fg", "Test body") + + assert result == {"data": {"comment": {"id": "123"}}} + mock_execute.assert_called_once() + + @patch.object(GitHubService, "execute_graphql_query") + @patch.object(GitHubService, "_determine_reply_strategy") + @patch.object(GitHubService, "_get_review_id_for_comment") + def test_post_reply_comment_strategy_with_review_lookup( + self, mock_get_review: Mock, mock_strategy: Mock, mock_execute: Mock + ) -> None: + """Test post_reply with comment strategy and review ID lookup.""" + mock_strategy.return_value = "comment_reply" + mock_get_review.return_value = "PRR_123" + mock_execute.return_value = {"data": {"comment": {"id": "456"}}} + + service = GitHubService() + result = service.post_reply("IC_kwDOABcD12MAAAABcDE3fg", "Test body") + + assert result == {"data": {"comment": {"id": "456"}}} + mock_get_review.assert_called_once_with("IC_kwDOABcD12MAAAABcDE3fg") + mock_execute.assert_called_once() + + @patch.object(GitHubService, "_determine_reply_strategy") + @patch.object(GitHubService, "_get_review_id_for_comment") + def test_post_reply_comment_strategy_no_review_id( + self, mock_get_review: Mock, mock_strategy: Mock + ) -> None: + """Test post_reply with comment strategy when review ID cannot be found.""" + mock_strategy.return_value = "comment_reply" + mock_get_review.return_value = None + + service = GitHubService() + with pytest.raises(ValueError) as exc_info: + service.post_reply("IC_kwDOABcD12MAAAABcDE3fg", "Test body") + + assert "Review ID is required" in str(exc_info.value) + + def test_resolve_thread_validation_errors(self) -> None: + """Test thread resolution with invalid inputs.""" + service = GitHubService() + + # Test empty thread ID + with pytest.raises(ValueError) as exc_info: + service.resolve_thread("") + assert "Thread ID cannot be empty" in str(exc_info.value) + + # Test whitespace-only thread ID + with pytest.raises(ValueError) as exc_info: + service.resolve_thread(" ") + assert "Thread ID cannot be empty" in str(exc_info.value) + + @patch.object(GitHubService, "execute_graphql_query") + def test_resolve_thread_invalid_response(self, mock_execute: Mock) -> None: + """Test thread resolution with invalid GraphQL response.""" + mock_execute.return_value = { + "data": {"resolveReviewThread": {}} + } # Missing thread data + + service = GitHubService() + # Method currently returns the response as-is without validation + result = service.resolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") + assert result == {"data": {"resolveReviewThread": {}}} + + @patch.object(GitHubService, "execute_graphql_query") + def test_get_review_id_for_comment_errors(self, mock_execute: Mock) -> None: + """Test error handling in review ID lookup.""" + service = GitHubService() + + # Mock the GraphQL query to return no review field + mock_execute.return_value = {"data": {"node": {}}} # No review field + + # This should be called through post_reply when review_id can't be determined + result = service._get_review_id_for_comment("IC_kwDOABcD12MAAAABcDE3fg") + assert result is None + + def test_determine_reply_strategy_patterns(self) -> None: + """Test reply strategy determination based on ID patterns.""" + service = GitHubService() + + # Test thread ID patterns - should return "thread_reply" + thread_ids = [ + "PRT_kwDOABcD12MAAAABcDE3fg", + "PRRT_kwDOABcD12MAAAABcDE3fg", + "RT_kwDOABcD12MAAAABcDE3fg", + ] + + for thread_id in thread_ids: + strategy = service._determine_reply_strategy(thread_id) + assert strategy == "thread_reply", f"Failed for {thread_id}" + + # Test comment ID patterns - should return "comment_reply" + comment_ids = [ + "IC_kwDOABcD12MAAAABcDE3fg", + "PRRC_kwDOABcD12MAAAABcDE3fg", + "RP_kwDOABcD12MAAAABcDE3fg", + ] + + for comment_id in comment_ids: + strategy = service._determine_reply_strategy(comment_id) + assert strategy == "comment_reply", f"Failed for {comment_id}" + + # Test unknown format - should default to "comment_reply" + unknown_ids = ["unknown_format", "123456", "UNKNOWN_kwDOABcD12MAAAABcDE3fg"] + + for unknown_id in unknown_ids: + strategy = service._determine_reply_strategy(unknown_id) + assert strategy == "comment_reply", f"Failed for {unknown_id}" + + @patch.object(GitHubService, "execute_graphql_query") + def test_get_review_id_for_comment_success(self, mock_execute: Mock) -> None: + """Test successful review ID lookup for comment.""" + mock_execute.return_value = { + "data": { + "node": {"pullRequestReview": {"id": "PRR_kwDOABcD12MAAAABcDE3fg"}} + } + } + + service = GitHubService() + review_id = service._get_review_id_for_comment("IC_kwDOABcD12MAAAABcDE3fg") + + assert review_id == "PRR_kwDOABcD12MAAAABcDE3fg" + mock_execute.assert_called_once() + + # Test missing review data + mock_execute.return_value = {"data": {"node": {}}} # No pullRequestReview field + + result = service._get_review_id_for_comment("IC_kwDOABcD12MAAAABcDE3fg") + assert result is None diff --git a/tests/unit/services/test_reply_service.py b/tests/unit/services/test_reply_service.py index a527a61..6939b73 100644 --- a/tests/unit/services/test_reply_service.py +++ b/tests/unit/services/test_reply_service.py @@ -247,6 +247,227 @@ def test_validate_comment_exists_not_found(self) -> None: assert exists is False + @patch.object(ReplyService, "_get_repository_info") + def test_post_reply_node_id_success(self, mock_get_repo_info: Mock) -> None: + """Test successful reply posting with node ID.""" + mock_get_repo_info.return_value = ("owner", "repo") + + mock_github_service = Mock(spec=GitHubService) + mock_response = { + "data": { + "addPullRequestReviewThreadReply": { + "comment": { + "id": "MDEyOklzc3VlQ29tbWVudDEyMzQ1Njc4OQ==", + "body": "Test reply body", + "author": {"login": "testuser"}, + "createdAt": "2023-01-01T12:00:00Z", + "url": "https://github.com/owner/repo/pull/1#discussion_r123456789", + } + } + } + } + mock_github_service.post_reply.return_value = mock_response + + service = ReplyService(mock_github_service) + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + result = service.post_reply(request) + + assert result is not None + mock_github_service.post_reply.assert_called_once() + + @patch.object(ReplyService, "_get_repository_info") + def test_post_reply_numeric_id_fallback(self, mock_get_repo_info: Mock) -> None: + """Test reply posting with numeric ID falls back to REST.""" + mock_get_repo_info.return_value = ("owner", "repo") + + mock_github_service = Mock(spec=GitHubService) + service = ReplyService(mock_github_service) + + # Mock the fallback method + service._post_reply_fallback_rest = Mock(return_value={"id": "123456789"}) + + request = ReplyRequest("123456789", "Test reply body") + result = service.post_reply(request) + + service._post_reply_fallback_rest.assert_called_once() + assert result == {"id": "123456789"} + + @patch.object(ReplyService, "_get_repository_info") + def test_post_reply_graphql_errors(self, mock_get_repo_info: Mock) -> None: + """Test GraphQL error handling in post_reply.""" + mock_get_repo_info.return_value = ("owner", "repo") + + mock_github_service = Mock(spec=GitHubService) + mock_response = {"errors": [{"message": "Comment not found"}]} + mock_github_service.post_reply.return_value = mock_response + + service = ReplyService(mock_github_service) + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + with pytest.raises(CommentNotFoundError): + service.post_reply(request) + + @patch.object(ReplyService, "_get_repository_info") + def test_post_reply_no_comment_data(self, mock_get_repo_info: Mock) -> None: + """Test post_reply when no comment data is returned.""" + mock_get_repo_info.return_value = ("owner", "repo") + + mock_github_service = Mock(spec=GitHubService) + mock_response = {"data": {"addPullRequestReviewThreadReply": {}}} + mock_github_service.post_reply.return_value = mock_response + + service = ReplyService(mock_github_service) + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + with pytest.raises(ReplyServiceError) as exc_info: + service.post_reply(request) + + assert "No comment data returned" in str(exc_info.value) + + def test_post_reply_invalid_comment_id(self) -> None: + """Test post_reply with invalid comment ID raises ValueError.""" + mock_github_service = Mock(spec=GitHubService) + service = ReplyService(mock_github_service) + + # Mock _get_repository_info to succeed, but make github_service.post_reply fail + with patch.object( + service, "_get_repository_info", return_value=("owner", "repo") + ): + # Mock post_reply to raise ValueError for invalid format + mock_github_service.post_reply.side_effect = ValueError("Invalid format") + + request = ReplyRequest("invalid_node_id", "Test reply body") + + with pytest.raises(ReplyServiceError) as exc_info: + service.post_reply(request) + + assert "Invalid comment ID" in str(exc_info.value) + + @patch.object(ReplyService, "_get_repository_info") + def test_post_reply_github_api_error(self, mock_get_repo_info: Mock) -> None: + """Test post_reply with GitHubAPIError.""" + mock_get_repo_info.return_value = ("owner", "repo") + + mock_github_service = Mock(spec=GitHubService) + mock_github_service.post_reply.side_effect = GitHubAPIError("API error") + + service = ReplyService(mock_github_service) + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + with pytest.raises(ReplyServiceError) as exc_info: + service.post_reply(request) + + assert "Failed to post reply" in str(exc_info.value) + + @patch.object(ReplyService, "_get_repository_info") + def test_post_reply_github_api_not_found_error( + self, mock_get_repo_info: Mock + ) -> None: + """Test post_reply with GitHubAPIError for not found.""" + mock_get_repo_info.return_value = ("owner", "repo") + + mock_github_service = Mock(spec=GitHubService) + mock_github_service.post_reply.side_effect = GitHubAPIError("Comment not found") + + service = ReplyService(mock_github_service) + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + with pytest.raises(CommentNotFoundError): + service.post_reply(request) + + def test_handle_graphql_errors_not_found(self) -> None: + """Test _handle_graphql_errors with not found error.""" + service = ReplyService() + errors = [{"message": "Comment not found"}] + + with pytest.raises(CommentNotFoundError): + service._handle_graphql_errors(errors, "123") + + def test_handle_graphql_errors_does_not_exist(self) -> None: + """Test _handle_graphql_errors with does not exist error.""" + service = ReplyService() + errors = [{"message": "Resource does not exist"}] + + with pytest.raises(CommentNotFoundError): + service._handle_graphql_errors(errors, "123") + + def test_handle_graphql_errors_generic(self) -> None: + """Test _handle_graphql_errors with generic error.""" + service = ReplyService() + errors = [{"message": "Generic error"}, {"message": "Another error"}] + + with pytest.raises(ReplyServiceError) as exc_info: + service._handle_graphql_errors(errors, "123") + + assert "Generic error; Another error" in str(exc_info.value) + + def test_handle_graphql_errors_no_message(self) -> None: + """Test _handle_graphql_errors with error without message.""" + service = ReplyService() + errors = [{"type": "UNKNOWN", "path": ["field"]}] + + with pytest.raises(ReplyServiceError) as exc_info: + service._handle_graphql_errors(errors, "123") + + # Should use str representation of error dict + assert "type" in str(exc_info.value) + + def test_build_reply_info_from_graphql_with_context(self) -> None: + """Test _build_reply_info_from_graphql with fetch_context=True.""" + mock_github_service = Mock(spec=GitHubService) + service = ReplyService(mock_github_service) + + # Mock the context fetching method + service._get_parent_comment_info = Mock(return_value={"parent": "context"}) + + comment_data = { + "id": "MDEyOklzc3VlQ29tbWVudDEyMzQ1Njc4OQ==", + "body": "Test reply", + "author": {"login": "testuser"}, + "createdAt": "2023-01-01T12:00:00Z", + "url": "https://github.com/owner/repo/pull/1#discussion_r123456789", + } + + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + result = service._build_reply_info_from_graphql( + comment_data, request, fetch_context=True, owner="owner", repo="repo" + ) + + service._get_parent_comment_info.assert_called_once() + assert "parent" in result + assert result["parent"] == "context" + + def test_build_reply_info_from_graphql_without_context(self) -> None: + """Test _build_reply_info_from_graphql with fetch_context=False.""" + mock_github_service = Mock(spec=GitHubService) + service = ReplyService(mock_github_service) + + comment_data = { + "id": "MDEyOklzc3VlQ29tbWVudDEyMzQ1Njc4OQ==", + "body": "Test reply", + "author": {"login": "testuser"}, + "createdAt": "2023-01-01T12:00:00Z", + "url": "https://github.com/owner/repo/pull/1#discussion_r123456789", + } + + request = ReplyRequest("RC_kwDOABCDEF4AaAaA", "Test reply body") + + result = service._build_reply_info_from_graphql( + comment_data, request, fetch_context=False, owner="owner", repo="repo" + ) + + # Check the actual keys returned by the method + assert "reply_id" in result + assert "reply_url" in result + assert "comment_id" in result + assert "created_at" in result + assert "author" in result + assert "body_preview" in result + assert result["comment_id"] == "RC_kwDOABCDEF4AaAaA" + assert result["author"] == "testuser" + def test_validate_comment_exists_invalid_response(self) -> None: """Test comment validation with invalid JSON response.""" mock_github_service = Mock(spec=GitHubService) diff --git a/tests/unit/services/test_resolve_service.py b/tests/unit/services/test_resolve_service.py index a0a2418..14722ec 100644 --- a/tests/unit/services/test_resolve_service.py +++ b/tests/unit/services/test_resolve_service.py @@ -798,3 +798,101 @@ def test_graphql_mutation_includes_pr_info_fields(self) -> None: assert "pullRequest" in UNRESOLVE_THREAD_MUTATION assert "number" in UNRESOLVE_THREAD_MUTATION assert "nameWithOwner" in UNRESOLVE_THREAD_MUTATION + + +class TestResolveServiceErrorCoverage: + """Test error cases for ResolveService to improve coverage.""" + + def test_resolve_thread_generic_exception_during_mutation(self) -> None: + """Test handling of generic exception during GraphQL mutation.""" + mock_github_service = Mock(spec=GitHubService) + # Make execute_graphql_query raise a generic exception (not GitHubAPIError) + mock_github_service.execute_graphql_query.side_effect = RuntimeError( + "Random error" + ) + + service = ResolveService(mock_github_service) + + with pytest.raises(GitHubAPIError, match="Failed to execute resolve mutation"): + service.resolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") + + def test_resolve_thread_response_structure_error(self) -> None: + """Test handling of KeyError/TypeError in response structure.""" + mock_github_service = Mock(spec=GitHubService) + # Return a response that will cause KeyError when accessing fields + mock_github_service.execute_graphql_query.return_value = { + "data": { + "resolveReviewThread": { + # Missing 'thread' key - will cause KeyError in thread_data access + } + } + } + + service = ResolveService(mock_github_service) + + with pytest.raises(ResolveServiceError, match="No thread data returned"): + service.resolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") + + def test_resolve_thread_unexpected_exception(self) -> None: + """Test handling of unexpected exception during resolution.""" + mock_github_service = Mock(spec=GitHubService) + + # Make the service methods raise an unexpected exception + def raise_unexpected(*args, **kwargs): + raise ValueError("Unexpected error during processing") + + mock_github_service.execute_graphql_query.side_effect = raise_unexpected + + service = ResolveService(mock_github_service) + + with pytest.raises(GitHubAPIError, match="Failed to execute resolve mutation"): + service.resolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") + + def test_unresolve_thread_generic_exception_during_mutation(self) -> None: + """Test handling of generic exception during unresolve GraphQL mutation.""" + mock_github_service = Mock(spec=GitHubService) + # Make execute_graphql_query raise a generic exception (not GitHubAPIError) + mock_github_service.execute_graphql_query.side_effect = RuntimeError( + "Random error" + ) + + service = ResolveService(mock_github_service) + + with pytest.raises( + GitHubAPIError, match="Failed to execute unresolve mutation" + ): + service.unresolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") + + def test_unresolve_thread_response_structure_error(self) -> None: + """Test handling of response structure error in unresolve.""" + mock_github_service = Mock(spec=GitHubService) + # Return a response that will cause KeyError when accessing fields + mock_github_service.execute_graphql_query.return_value = { + "data": { + "unresolveReviewThread": { + # Missing 'thread' key - will cause error + } + } + } + + service = ResolveService(mock_github_service) + + with pytest.raises(ResolveServiceError, match="No thread data returned"): + service.unresolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") + + def test_unresolve_thread_unexpected_exception(self) -> None: + """Test handling of unexpected exception during unresolve.""" + mock_github_service = Mock(spec=GitHubService) + + # Make the service methods raise an unexpected exception + def raise_unexpected(*args, **kwargs): + raise ValueError("Unexpected error during processing") + + mock_github_service.execute_graphql_query.side_effect = raise_unexpected + + service = ResolveService(mock_github_service) + + with pytest.raises( + GitHubAPIError, match="Failed to execute unresolve mutation" + ): + service.unresolve_thread("PRT_kwDOABcD12MAAAABcDE3fg") diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py new file mode 100644 index 0000000..130b9fd --- /dev/null +++ b/tests/unit/test_cli.py @@ -0,0 +1,476 @@ +"""Unit tests for the CLI module (src/toady/cli.py). + +This module provides comprehensive unit tests for the main CLI interface, +including group setup, version display, command registration, context handling, +error handling, and the main entry point. +""" + +import os +from unittest.mock import Mock, patch + +import click +from click.testing import CliRunner +import pytest + +from toady import __version__ +from toady.cli import cli, main +from toady.exceptions import ( + ToadyError, +) + + +class TestCLIGroupDefinition: + """Test the main CLI group definition and configuration.""" + + def test_cli_is_group(self): + """Test that cli is a Click group.""" + assert isinstance(cli, click.Group) + assert cli.name == "cli" + + def test_cli_has_version_option(self): + """Test that CLI has version option configured.""" + # Check that version option is present + version_option = None + for param in cli.params: + if isinstance(param, click.Option) and "--version" in param.opts: + version_option = param + break + + assert version_option is not None + assert version_option.is_flag is True + + def test_cli_has_debug_option(self): + """Test that CLI has debug option configured.""" + debug_option = None + for param in cli.params: + if isinstance(param, click.Option) and "--debug" in param.opts: + debug_option = param + break + + assert debug_option is not None + assert debug_option.is_flag is True + assert debug_option.envvar == "TOADY_DEBUG" + + def test_cli_callback_function_exists(self): + """Test that CLI callback function is properly defined.""" + assert cli.callback is not None + assert callable(cli.callback) + + def test_cli_docstring_present(self): + """Test that CLI has comprehensive help documentation.""" + assert cli.help is not None + assert "Toady - GitHub PR review management tool" in cli.help + assert "PREREQUISITES:" in cli.help + assert "CORE WORKFLOW:" in cli.help + assert "TROUBLESHOOTING:" in cli.help + + +class TestCLIGroupFunctionality: + """Test the functional behavior of the CLI group.""" + + def test_cli_version_display(self, runner): + """Test version display functionality.""" + result = runner.invoke(cli, ["--version"]) + assert result.exit_code == 0 + assert __version__ in result.output + assert "toady" in result.output.lower() + + def test_cli_help_display(self, runner): + """Test help display functionality.""" + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + assert "Toady - GitHub PR review management tool" in result.output + assert "Commands:" in result.output + assert "--debug" in result.output + assert "--version" in result.output + + def test_cli_debug_flag_sets_context(self, runner): + """Test that debug flag is properly stored in context.""" + # Use existing fetch command to test context passing + with patch("toady.commands.fetch.FetchService") as mock_service_class: + mock_service = Mock() + mock_service.fetch_review_threads_with_pr_selection.return_value = ( + [], + None, + ) + mock_service_class.return_value = mock_service + + with patch( + "toady.commands.fetch.resolve_format_from_options" + ) as mock_resolve_format: + mock_resolve_format.return_value = "json" + + # Test that debug context is available + # (command should run without error) + result = runner.invoke(cli, ["--debug", "fetch"]) + # Command exits early when no PR selected, which is fine + assert result.exit_code == 0 + + def test_cli_context_object_initialization(self, runner): + """Test that context object is properly initialized.""" + # Test using actual command to verify context is set up + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + # If CLI ran successfully, context was initialized properly + + def test_cli_debug_environment_variable(self, runner): + """Test that debug option respects TOADY_DEBUG environment variable.""" + # Test with environment variable set + with patch.dict(os.environ, {"TOADY_DEBUG": "1"}): + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + # If command runs, environment variable was processed + + with patch.dict(os.environ, {"TOADY_DEBUG": "0"}): + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + + +class TestCLICommandRegistration: + """Test that all expected commands are properly registered.""" + + def test_all_commands_registered(self, runner): + """Test that all expected commands are registered with the CLI group.""" + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + + expected_commands = ["fetch", "reply", "resolve", "schema"] + for command in expected_commands: + assert command in result.output + + def test_registered_commands_are_callable(self): + """Test that all registered commands are callable.""" + expected_commands = ["fetch", "reply", "resolve", "schema"] + for command_name in expected_commands: + command = cli.get_command(None, command_name) + assert command is not None + assert callable(command) + + def test_command_help_accessible(self, runner): + """Test that help is accessible for all registered commands.""" + expected_commands = ["fetch", "reply", "resolve", "schema"] + for command_name in expected_commands: + result = runner.invoke(cli, [command_name, "--help"]) + assert result.exit_code == 0 + assert "Usage:" in result.output + + def test_invalid_command_handling(self, runner): + """Test handling of invalid/unknown commands.""" + result = runner.invoke(cli, ["invalid-command"]) + assert result.exit_code != 0 + assert "No such command 'invalid-command'" in result.output + + +class TestCLIContextHandling: + """Test CLI context object handling and propagation.""" + + def test_context_object_structure(self, runner): + """Test that context object has expected structure.""" + # Test by invoking CLI with known commands and checking success + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + # Context is working if CLI processes successfully + + def test_context_propagation_to_subcommands(self, runner): + """Test that context is properly propagated to subcommands.""" + # Test subcommand help which requires context propagation + result = runner.invoke(cli, ["--debug", "fetch", "--help"]) + assert result.exit_code == 0 + # If help displays, context was propagated properly + + def test_context_ensure_object(self, runner): + """Test that context.ensure_object works correctly.""" + # Test by running commands that depend on context + result = runner.invoke(cli, ["schema", "--help"]) + assert result.exit_code == 0 + # Context ensure_object worked if command ran successfully + + +class TestMainEntryPoint: + """Test the main() entry point function.""" + + @patch("toady.cli.cli") + def test_main_calls_cli(self, mock_cli): + """Test that main() calls the CLI function.""" + main() + mock_cli.assert_called_once() + + @patch("toady.cli.handle_error") + @patch("toady.cli.cli") + def test_main_handles_toady_error(self, mock_cli, mock_handle_error): + """Test that main() properly handles ToadyError exceptions.""" + test_error = ToadyError("Test error") + mock_cli.side_effect = test_error + + main() + + mock_handle_error.assert_called_once_with(test_error, show_traceback=False) + + @patch("toady.cli.handle_error") + @patch("toady.cli.cli") + def test_main_handles_toady_error_with_debug_env(self, mock_cli, mock_handle_error): + """Test that main() handles ToadyError with debug environment variable.""" + test_error = ToadyError("Test error") + mock_cli.side_effect = test_error + + with patch.dict(os.environ, {"TOADY_DEBUG": "1"}): + main() + + mock_handle_error.assert_called_once_with(test_error, show_traceback=True) + + @patch("toady.cli.handle_error") + @patch("toady.cli.cli") + def test_main_handles_toady_error_with_debug_env_variations( + self, mock_cli, mock_handle_error + ): + """Test that main() handles various debug environment variable values.""" + test_error = ToadyError("Test error") + mock_cli.side_effect = test_error + + debug_values = ["true", "TRUE", "yes", "YES", "1"] + for debug_value in debug_values: + mock_handle_error.reset_mock() + with patch.dict(os.environ, {"TOADY_DEBUG": debug_value}): + main() + mock_handle_error.assert_called_once_with(test_error, show_traceback=True) + + @patch("toady.cli.cli") + @patch("toady.cli.handle_error") + def test_main_handles_unexpected_error_normal_mode( + self, mock_handle_error, mock_cli + ): + """Test that main() handles unexpected exceptions in normal mode.""" + test_error = ValueError("Unexpected error") + mock_cli.side_effect = test_error + + main() + + mock_handle_error.assert_called_once_with(test_error, show_traceback=False) + + @patch("toady.cli.cli") + def test_main_handles_unexpected_error_debug_mode(self, mock_cli): + """Test that main() re-raises unexpected exceptions in debug mode.""" + test_error = ValueError("Unexpected error") + mock_cli.side_effect = test_error + + with ( + patch.dict(os.environ, {"TOADY_DEBUG": "1"}), + pytest.raises(ValueError, match="Unexpected error"), + ): + main() + + @patch("toady.cli.cli") + @patch("toady.cli.handle_error") + def test_main_debug_environment_parsing(self, mock_handle_error, mock_cli): + """Test that main() correctly parses debug environment variable.""" + test_error = ToadyError("Test error") + mock_cli.side_effect = test_error + + # Test falsy values + falsy_values = ["0", "false", "FALSE", "no", "NO", ""] + for debug_value in falsy_values: + mock_handle_error.reset_mock() + with patch.dict(os.environ, {"TOADY_DEBUG": debug_value}): + main() + mock_handle_error.assert_called_once_with(test_error, show_traceback=False) + + +class TestCLIErrorHandling: + """Test error handling within the CLI interface.""" + + def test_cli_handles_click_exceptions(self, runner): + """Test that CLI properly handles Click exceptions.""" + # Test with missing required option (should be handled by Click) + result = runner.invoke(cli, ["fetch", "--pr"]) # Missing PR number + assert result.exit_code != 0 + + def test_cli_parameter_validation(self, runner): + """Test that CLI validates parameters correctly.""" + # Test invalid command structure + result = runner.invoke(cli, ["--invalid-option"]) + assert result.exit_code != 0 + assert "No such option" in result.output + + def test_cli_handles_keyboard_interrupt(self, runner): + """Test that CLI handles KeyboardInterrupt gracefully.""" + # Test with Ctrl+C simulation in Click runner + result = runner.invoke(cli, ["nonexistent-command"]) + # Invalid command should result in non-zero exit code + assert result.exit_code != 0 + + def test_cli_context_exception_handling(self, runner): + """Test that CLI context exceptions are handled properly.""" + # Test that context is properly accessible in real commands + result = runner.invoke(cli, ["fetch", "--help"]) + assert result.exit_code == 0 + # If command runs, context was accessible + + +class TestCLIEnvironmentVariables: + """Test environment variable handling in the CLI.""" + + def test_toady_debug_environment_variable_handling(self, runner): + """Test comprehensive TOADY_DEBUG environment variable handling.""" + # Test various truthy values + truthy_values = ["1", "true", "TRUE", "yes", "YES"] + for value in truthy_values: + with patch.dict(os.environ, {"TOADY_DEBUG": value}): + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 # Command should run successfully + + # Test various falsy values + falsy_values = ["0", "false", "FALSE", "no", "NO", ""] + for value in falsy_values: + with patch.dict(os.environ, {"TOADY_DEBUG": value}): + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 # Command should run successfully + + def test_environment_variable_precedence(self, runner): + """Test that command line flag takes precedence over environment variable.""" + # Environment says False, but command line says True + with patch.dict(os.environ, {"TOADY_DEBUG": "0"}): + result = runner.invoke(cli, ["--debug", "--help"]) + assert result.exit_code == 0 # Command should run successfully + + # Environment says True, but no command line flag + with patch.dict(os.environ, {"TOADY_DEBUG": "1"}): + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 # Command should run successfully + + def test_missing_environment_variable(self, runner): + """Test behavior when TOADY_DEBUG environment variable is not set.""" + # Ensure environment variable is not set + with patch.dict(os.environ, {}, clear=True): + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 # Command should run with default debug=False + + +class TestCLIIntegration: + """Test integration aspects of the CLI.""" + + def test_cli_with_real_commands(self, runner): + """Test that CLI works with actual registered commands.""" + # Test that we can get help for each command + commands = ["fetch", "reply", "resolve", "schema"] + for command in commands: + result = runner.invoke(cli, [command, "--help"]) + assert result.exit_code == 0 + assert command in result.output.lower() + + def test_cli_version_matches_package_version(self, runner): + """Test that CLI version matches the package version.""" + result = runner.invoke(cli, ["--version"]) + assert result.exit_code == 0 + assert __version__ in result.output + + def test_cli_help_structure(self, runner): + """Test that CLI help has expected structure and sections.""" + result = runner.invoke(cli, ["--help"]) + assert result.exit_code == 0 + + # Check for major sections + expected_sections = [ + "Usage:", + "Options:", + "Commands:", + "PREREQUISITES:", + "CORE WORKFLOW:", + "AGENT-FRIENDLY USAGE:", + "TROUBLESHOOTING:", + ] + + for section in expected_sections: + assert section in result.output + + def test_cli_group_attributes(self): + """Test that CLI group has expected attributes.""" + assert hasattr(cli, "commands") + assert hasattr(cli, "params") + assert hasattr(cli, "callback") + assert hasattr(cli, "help") + + # Test that commands dictionary contains expected commands + expected_commands = ["fetch", "reply", "resolve", "schema"] + for command in expected_commands: + assert command in cli.commands + + +class TestCLIMainBehavior: + """Test main() function behavior patterns.""" + + @patch("sys.exit") + @patch("toady.cli.handle_error") + @patch("toady.cli.cli") + def test_main_exit_handling(self, mock_cli, mock_handle_error, mock_exit): + """Test that main() properly exits after handling errors.""" + test_error = ToadyError("Test error") + mock_cli.side_effect = test_error + + # handle_error should call sys.exit, but we're mocking it + main() + + mock_handle_error.assert_called_once() + + @patch("toady.cli.cli") + def test_main_normal_execution(self, mock_cli): + """Test that main() executes normally without errors.""" + mock_cli.return_value = None + + # Should not raise any exceptions + main() + + mock_cli.assert_called_once() + + @patch.dict("os.environ", {"TOADY_DEBUG": "1"}) + @patch("toady.cli.cli") + @patch("toady.cli.handle_error") + def test_main_debug_environment_access(self, mock_handle_error, mock_cli): + """Test that main() correctly accesses debug environment variable.""" + test_error = ToadyError("Test error") + mock_cli.side_effect = test_error + + main() + + # Should call handle_error with show_traceback=True when TOADY_DEBUG is set + mock_handle_error.assert_called_once_with(test_error, show_traceback=True) + + +class TestCLIEdgeCases: + """Test edge cases and boundary conditions.""" + + def test_cli_with_empty_context(self, runner): + """Test CLI behavior when context object operations are involved.""" + # Test that context operations work with real commands + result = runner.invoke(cli, ["--debug", "reply", "--help"]) + assert result.exit_code == 0 + # If command runs, context manipulation works properly + + def test_cli_multiple_option_combinations(self, runner): + """Test CLI with multiple option combinations.""" + # Test various flag combinations + result = runner.invoke(cli, ["--debug", "--help"]) + assert result.exit_code == 0 + assert "--debug" in result.output + + def test_cli_with_invalid_subcommand_args(self, runner): + """Test CLI handling of invalid arguments to subcommands.""" + # This should show the main help since 'invalid' is not a command + result = runner.invoke(cli, ["invalid", "--some-option"]) + assert result.exit_code != 0 + assert "No such command 'invalid'" in result.output + + def test_cli_case_sensitivity(self, runner): + """Test that CLI commands are case sensitive.""" + result = runner.invoke(cli, ["FETCH"]) # Uppercase + assert result.exit_code != 0 + assert "No such command 'FETCH'" in result.output + + result = runner.invoke(cli, ["Fetch"]) # Mixed case + assert result.exit_code != 0 + assert "No such command 'Fetch'" in result.output + + +@pytest.fixture +def runner(): + """Create a Click CLI test runner.""" + return CliRunner() diff --git a/tests/unit/test_command_utils.py b/tests/unit/test_command_utils.py index 3e8d73e..51119fe 100644 --- a/tests/unit/test_command_utils.py +++ b/tests/unit/test_command_utils.py @@ -13,6 +13,7 @@ from toady.exceptions import ToadyError, ValidationError +@pytest.mark.unit class TestValidatePrNumber: """Test PR number validation.""" @@ -49,6 +50,7 @@ def test_oversized_pr_number(self): assert exc_info.value.param_hint == "--pr" +@pytest.mark.unit class TestValidateLimit: """Test limit validation.""" @@ -97,6 +99,7 @@ def test_oversized_limit_custom_max(self): assert exc_info.value.param_hint == "--limit" +@pytest.mark.unit class TestHandleCommandErrors: """Test command error handling decorator.""" @@ -211,7 +214,6 @@ def test_function_metadata_preservation(self): @handle_command_errors def test_function(): """Test function docstring.""" - pass assert test_function.__name__ == "test_function" assert test_function.__doc__ == "Test function docstring." diff --git a/tests/unit/test_error_handling.py b/tests/unit/test_error_handling.py index 2dee939..78d9083 100644 --- a/tests/unit/test_error_handling.py +++ b/tests/unit/test_error_handling.py @@ -130,10 +130,18 @@ def test_format_toady_error_with_suggestions(self): def test_format_unexpected_error(self): """Test formatting of unexpected error.""" error = ValueError("Unexpected error") - formatted = ErrorMessageFormatter.format_error(error) + # Test without debug mode โ€“ explicitly disable the flag + with patch.dict("os.environ", {}, clear=True): + formatted = ErrorMessageFormatter.format_error(error) assert "โŒ An unexpected error occurred" in formatted - assert "Error details: Unexpected error" in formatted + assert "Error details: Unexpected error" not in formatted + + # Test with debug mode enabled + with patch.dict("os.environ", {"TOADY_DEBUG": "1"}): + formatted_debug = ErrorMessageFormatter.format_error(error) + assert "โŒ An unexpected error occurred" in formatted_debug + assert "Error details: Unexpected error" in formatted_debug def test_get_exit_code_github_errors(self): """Test exit code mapping for GitHub errors.""" diff --git a/tests/unit/test_format_selection.py b/tests/unit/test_format_selection.py index a030cda..193462c 100644 --- a/tests/unit/test_format_selection.py +++ b/tests/unit/test_format_selection.py @@ -1,13 +1,20 @@ """Tests for format selection utilities.""" import os -from unittest.mock import patch +from unittest.mock import MagicMock, patch import pytest from toady.formatters.format_selection import ( FormatSelectionError, + _ensure_formatters_registered, + create_format_option, create_formatter, + create_legacy_pretty_option, + format_error_message, + format_object_output, + format_success_message, + format_threads_output, get_default_format, resolve_format_from_options, validate_format, @@ -207,3 +214,278 @@ def test_option_precedence_order(self): # Env should be used when no options given format_name = resolve_format_from_options(None, False) assert format_name == "pretty" + + +class TestEnsureFormattersRegistered: + """Test formatter registration functionality.""" + + @patch("toady.formatters.format_selection.FormatterFactory.list_formatters") + def test_ensure_formatters_json_not_registered(self, mock_list_formatters): + """Test JSON formatter registration when not present.""" + mock_list_formatters.return_value = [] + + with patch( + "toady.formatters.format_selection.FormatterFactory.register" + ) as mock_register: + _ensure_formatters_registered() + + # Should try to register JSON formatter + mock_register.assert_called() + + @patch("toady.formatters.format_selection.FormatterFactory.list_formatters") + @patch("toady.formatters.format_selection.FormatterFactory.register") + def test_ensure_formatters_json_import_error( + self, mock_register, mock_list_formatters + ): + """Test fallback JSON formatter registration on import error.""" + mock_list_formatters.return_value = [] + + # Simulate the specific import failing by manipulating sys.modules + import sys + + original_modules = sys.modules.copy() + + # Remove the json_formatter module if it exists + if "toady.formatters.json_formatter" in sys.modules: + del sys.modules["toady.formatters.json_formatter"] + + # Mock an import error when trying to import JSONFormatter + def mock_import(name, *args, **kwargs): + if "json_formatter" in name: + raise ImportError("Cannot import JSONFormatter") + return original_import(name, *args, **kwargs) + + original_import = __import__ + with patch("builtins.__import__", side_effect=mock_import): + _ensure_formatters_registered() + + # Should register simple JSON formatter as fallback + assert mock_register.call_count >= 1 + + # Restore original modules + sys.modules.update(original_modules) + + @patch("toady.formatters.format_selection.FormatterFactory.list_formatters") + def test_ensure_formatters_pretty_not_registered(self, mock_list_formatters): + """Test pretty formatter registration when not present.""" + mock_list_formatters.return_value = ["json"] # JSON present, pretty not + + with patch( + "toady.formatters.format_selection.FormatterFactory.register" + ) as mock_register: + _ensure_formatters_registered() + + # Should attempt to import pretty formatter + mock_register.assert_called() + + @patch("toady.formatters.format_selection.FormatterFactory.list_formatters") + def test_ensure_formatters_pretty_import_error(self, mock_list_formatters): + """Test handling of pretty formatter import error.""" + mock_list_formatters.return_value = ["json"] # JSON present, pretty not + + with patch("toady.formatters.format_selection.FormatterFactory.register"): + # Mock import error for PrettyFormatter + with patch("builtins.__import__", side_effect=ImportError): + _ensure_formatters_registered() + + # Should not raise error, just continue + + +class TestFormatOutputFunctions: + """Test format output utility functions.""" + + def test_format_threads_output_json(self): + """Test formatting threads output as JSON.""" + threads = [MagicMock()] + threads[0].to_dict.return_value = {"id": "1", "resolved": False} + + with patch( + "toady.formatters.format_selection.format_fetch_output" + ) as mock_format: + format_threads_output(threads, "json") + mock_format.assert_called_once_with(threads=threads, pretty=False) + + def test_format_threads_output_pretty(self): + """Test formatting threads output as pretty.""" + threads = [MagicMock()] + threads[0].to_dict.return_value = {"id": "1", "resolved": False} + + with patch( + "toady.formatters.format_selection.format_fetch_output" + ) as mock_format: + format_threads_output(threads, "pretty") + mock_format.assert_called_once_with(threads=threads, pretty=True) + + def test_format_threads_output_other_format(self): + """Test formatting threads output with other format.""" + threads = [MagicMock()] + mock_formatter = MagicMock() + mock_formatter.format_threads.return_value = "formatted output" + + with patch( + "toady.formatters.format_selection.create_formatter", + return_value=mock_formatter, + ) as mock_create: + with patch("click.echo") as mock_echo: + format_threads_output(threads, "custom") + + mock_create.assert_called_once_with("custom") + mock_formatter.format_threads.assert_called_once_with(threads) + mock_echo.assert_called_once_with("formatted output") + + def test_format_object_output_json(self): + """Test formatting object output as JSON.""" + obj = {"test": "data"} + + with patch("click.echo") as mock_echo: + format_object_output(obj, "json") + + mock_echo.assert_called_once() + # Verify JSON format in output + call_args = mock_echo.call_args[0][0] + assert '"test"' in call_args + assert '"data"' in call_args + + def test_format_object_output_pretty(self): + """Test formatting object output as pretty.""" + obj = {"test": "data"} + mock_formatter = MagicMock() + mock_formatter.format_object.return_value = "pretty output" + + with patch( + "toady.formatters.format_selection.create_formatter", + return_value=mock_formatter, + ) as mock_create: + with patch("click.echo") as mock_echo: + format_object_output(obj, "pretty") + + mock_create.assert_called_once_with("pretty") + mock_formatter.format_object.assert_called_once_with(obj) + mock_echo.assert_called_once_with("pretty output") + + def test_format_object_output_other_format(self): + """Test formatting object output with other format.""" + obj = {"test": "data"} + mock_formatter = MagicMock() + mock_formatter.format_object.return_value = "custom output" + + with patch( + "toady.formatters.format_selection.create_formatter", + return_value=mock_formatter, + ) as mock_create: + with patch("click.echo") as mock_echo: + format_object_output(obj, "custom") + + mock_create.assert_called_once_with("custom") + mock_formatter.format_object.assert_called_once_with(obj) + mock_echo.assert_called_once_with("custom output") + + def test_format_success_message_json(self): + """Test formatting success message as JSON.""" + message = "Operation successful" + details = {"operation": "test"} + + with patch("click.echo") as mock_echo: + format_success_message(message, "json", details) + + mock_echo.assert_called_once() + call_args = mock_echo.call_args[0][0] + assert '"success": true' in call_args + assert '"message": "Operation successful"' in call_args + assert '"details"' in call_args + + def test_format_success_message_json_no_details(self): + """Test formatting success message as JSON without details.""" + message = "Operation successful" + + with patch("click.echo") as mock_echo: + format_success_message(message, "json") + + mock_echo.assert_called_once() + call_args = mock_echo.call_args[0][0] + assert '"success": true' in call_args + assert '"message": "Operation successful"' in call_args + assert '"details"' not in call_args + + def test_format_success_message_other_format(self): + """Test formatting success message with other format.""" + message = "Operation successful" + details = {"operation": "test"} + mock_formatter = MagicMock() + mock_formatter.format_success_message.return_value = "success output" + + with patch( + "toady.formatters.format_selection.create_formatter", + return_value=mock_formatter, + ) as mock_create: + with patch("click.echo") as mock_echo: + format_success_message(message, "custom", details) + + mock_create.assert_called_once_with("custom") + mock_formatter.format_success_message.assert_called_once_with( + message, details + ) + mock_echo.assert_called_once_with("success output") + + def test_format_error_message_json(self): + """Test formatting error message as JSON.""" + error = {"error": "Something went wrong", "code": 500} + + with patch("click.echo") as mock_echo: + format_error_message(error, "json") + + mock_echo.assert_called_once() + call_args = mock_echo.call_args + # Should be called with err=True + assert call_args[1]["err"] is True + output = call_args[0][0] + assert '"error": "Something went wrong"' in output + + def test_format_error_message_other_format(self): + """Test formatting error message with other format.""" + error = {"error": "Something went wrong", "code": 500} + mock_formatter = MagicMock() + mock_formatter.format_error.return_value = "error output" + + with patch( + "toady.formatters.format_selection.create_formatter", + return_value=mock_formatter, + ) as mock_create: + with patch("click.echo") as mock_echo: + format_error_message(error, "custom") + + mock_create.assert_called_once_with("custom") + mock_formatter.format_error.assert_called_once_with(error) + mock_echo.assert_called_once_with("error output", err=True) + + +class TestClickOptions: + """Test Click option creation utilities.""" + + def test_create_format_option_basic(self): + """Test creating a basic format option.""" + option_decorator = create_format_option() + + # Should return a callable decorator + assert callable(option_decorator) + + def test_create_format_option_with_kwargs(self): + """Test creating format option with additional kwargs.""" + option_decorator = create_format_option(default="json", required=False) + + # Should return a callable decorator + assert callable(option_decorator) + + def test_create_legacy_pretty_option_basic(self): + """Test creating a basic legacy pretty option.""" + option_decorator = create_legacy_pretty_option() + + # Should return a callable decorator + assert callable(option_decorator) + + def test_create_legacy_pretty_option_with_kwargs(self): + """Test creating legacy pretty option with additional kwargs.""" + option_decorator = create_legacy_pretty_option(hidden=True) + + # Should return a callable decorator + assert callable(option_decorator) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py new file mode 100644 index 0000000..e0e0bf4 --- /dev/null +++ b/tests/unit/test_utils.py @@ -0,0 +1,492 @@ +"""Unit tests for utility functions.""" + +from datetime import datetime +import json +from unittest.mock import patch + +import click +import pytest + +from toady.exceptions import ValidationError +from toady.utils import MAX_PR_NUMBER, emit_error, parse_datetime + + +@pytest.mark.unit +class TestConstants: + """Test module constants.""" + + def test_max_pr_number_constant(self): + """Test that MAX_PR_NUMBER has expected value.""" + assert MAX_PR_NUMBER == 999999 + assert isinstance(MAX_PR_NUMBER, int) + + +@pytest.mark.unit +class TestParseDatetime: + """Test the parse_datetime function.""" + + def test_valid_datetime_with_microseconds(self): + """Test parsing datetime with microseconds.""" + date_str = "2024-01-15T10:30:45.123456" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45, 123456) + assert result == expected + + def test_valid_datetime_without_microseconds(self): + """Test parsing datetime without microseconds.""" + date_str = "2024-01-15T10:30:45" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45) + assert result == expected + + def test_datetime_with_z_timezone(self): + """Test parsing datetime with Z timezone indicator.""" + date_str = "2024-01-15T10:30:45Z" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45) + assert result == expected + + def test_datetime_with_z_and_microseconds(self): + """Test parsing datetime with Z timezone and microseconds.""" + date_str = "2024-01-15T10:30:45.123456Z" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45, 123456) + assert result == expected + + def test_datetime_with_positive_timezone(self): + """Test parsing datetime with positive timezone offset.""" + date_str = "2024-01-15T10:30:45+05:00" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45) + assert result == expected + + def test_datetime_with_negative_timezone(self): + """Test parsing datetime with negative timezone offset.""" + date_str = "2024-01-15T10:30:45-05:00" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45) + assert result == expected + + def test_datetime_with_microseconds_and_positive_timezone(self): + """Test parsing datetime with microseconds and positive timezone.""" + date_str = "2024-01-15T10:30:45.123456+02:00" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45, 123456) + assert result == expected + + def test_datetime_with_microseconds_and_negative_timezone(self): + """Test parsing datetime with microseconds and negative timezone.""" + date_str = "2024-01-15T10:30:45.987654-08:00" + result = parse_datetime(date_str) + expected = datetime(2024, 1, 15, 10, 30, 45, 987654) + assert result == expected + + def test_invalid_input_type_none(self): + """Test parsing with None input.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime(None) + + error = exc_info.value + assert "Date string must be a string" in str(error) + assert error.field_name == "date_str" + + def test_invalid_input_type_integer(self): + """Test parsing with integer input.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime(12345) + + error = exc_info.value + assert "Date string must be a string" in str(error) + assert error.field_name == "date_str" + + def test_invalid_input_type_list(self): + """Test parsing with list input.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime(["2024-01-15"]) + + error = exc_info.value + assert "Date string must be a string" in str(error) + assert error.field_name == "date_str" + + def test_empty_string(self): + """Test parsing empty string.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime("") + + error = exc_info.value + assert "Date string cannot be empty" in str(error) + assert error.field_name == "date_str" + + def test_whitespace_only_string(self): + """Test parsing whitespace-only string.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime(" ") + + error = exc_info.value + assert "Date string cannot be empty" in str(error) + assert error.field_name == "date_str" + + def test_invalid_datetime_format(self): + """Test parsing invalid datetime format.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime("not-a-datetime") + + error = exc_info.value + assert "Unable to parse datetime" in str(error) + assert error.field_name == "date_str" + + def test_invalid_iso_format(self): + """Test parsing invalid ISO format.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024/01/15 10:30:45") + + error = exc_info.value + assert "Unable to parse datetime" in str(error) + assert error.field_name == "date_str" + + def test_partial_datetime(self): + """Test parsing partial datetime (date only).""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024-01-15") + + error = exc_info.value + assert "Unable to parse datetime" in str(error) + assert error.field_name == "date_str" + + def test_invalid_date_values(self): + """Test parsing with invalid date values.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024-13-45T25:70:90") + + error = exc_info.value + assert "Unable to parse datetime" in str(error) + assert error.field_name == "date_str" + + def test_malformed_timezone_format(self): + """Test parsing datetime with malformed timezone.""" + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024-01-15T10:30:45+invalid") + + error = exc_info.value + assert "Unable to parse datetime" in str(error) + assert error.field_name == "date_str" + + @pytest.mark.parametrize( + "date_str,expected", + [ + ("2024-01-15T10:30:45", datetime(2024, 1, 15, 10, 30, 45)), + ("2024-12-31T23:59:59.999999", datetime(2024, 12, 31, 23, 59, 59, 999999)), + ("2020-02-29T12:00:00", datetime(2020, 2, 29, 12, 0, 0)), # Leap year + ("2024-01-01T00:00:00.000001", datetime(2024, 1, 1, 0, 0, 0, 1)), + ], + ) + def test_various_valid_formats(self, date_str, expected): + """Test parsing various valid datetime formats.""" + result = parse_datetime(date_str) + assert result == expected + + def test_timezone_processing_exception_handling(self): + """Test exception handling during timezone processing.""" + # This test actually hits the type validation first + # The timezone processing only happens after type validation passes + # So let's test a string that would cause AttributeError during processing + with patch("toady.utils.datetime") as mock_datetime: + mock_datetime.strptime.side_effect = ValueError("test parsing error") + + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024-01-15T10:30:45+05:00") + + error = exc_info.value + assert "Unable to parse datetime" in str(error) + assert error.field_name == "date_str" + + def test_unexpected_exception_wrapping(self): + """Test that unexpected exceptions are wrapped in ValidationError.""" + # Mock datetime.strptime to raise an unexpected exception + with patch("toady.utils.datetime") as mock_datetime: + mock_datetime.strptime.side_effect = RuntimeError("Unexpected error") + + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024-01-15T10:30:45") + + error = exc_info.value + assert "Unexpected error parsing datetime" in str(error) + assert error.field_name == "date_str" + + def test_validation_error_re_raise(self): + """Test that ValidationError is re-raised without wrapping.""" + original_error = ValidationError("Original error") + + with patch("toady.utils.datetime") as mock_datetime: + mock_datetime.strptime.side_effect = original_error + + with pytest.raises(ValidationError) as exc_info: + parse_datetime("2024-01-15T10:30:45") + + # Should be the same ValidationError, not wrapped + assert exc_info.value is original_error + + +@pytest.mark.unit +class TestEmitError: + """Test the emit_error function.""" + + def test_emit_error_pretty_format(self): + """Test emit_error with pretty format.""" + ctx = click.Context(click.Command("test")) + + with ( + pytest.raises(click.exceptions.Exit) as exc_info, + patch("click.echo") as mock_echo, + ): + emit_error(ctx, 123, "test_error", "Test error message", True) + + assert exc_info.value.exit_code == 1 + mock_echo.assert_called_once_with("Test error message", err=True) + + def test_emit_error_json_format(self): + """Test emit_error with JSON format.""" + ctx = click.Context(click.Command("test")) + + with ( + pytest.raises(click.exceptions.Exit) as exc_info, + patch("click.echo") as mock_echo, + ): + emit_error(ctx, 123, "test_error", "Test error message", False) + + assert exc_info.value.exit_code == 1 + + # Verify JSON output + mock_echo.assert_called_once() + call_args = mock_echo.call_args + assert call_args[1]["err"] is True + + json_output = call_args[0][0] + parsed = json.loads(json_output) + + assert parsed == { + "pr_number": 123, + "success": False, + "error": "test_error", + "error_message": "Test error message", + } + + def test_emit_error_invalid_pr_number_zero(self): + """Test emit_error with zero PR number.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 0, "test_error", "Test message", False) + + # Should use fallback PR number of 0 + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["pr_number"] == 0 + + def test_emit_error_invalid_pr_number_negative(self): + """Test emit_error with negative PR number.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, -5, "test_error", "Test message", False) + + # Should use fallback PR number of 0 + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["pr_number"] == 0 + + def test_emit_error_invalid_pr_number_non_integer(self): + """Test emit_error with non-integer PR number.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, "not_an_int", "test_error", "Test message", False) + + # Should use fallback PR number of 0 + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["pr_number"] == 0 + + def test_emit_error_invalid_code_empty(self): + """Test emit_error with empty error code.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, "", "Test message", False) + + # Should use fallback error code + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["error"] == "UNKNOWN_ERROR" + + def test_emit_error_invalid_code_whitespace(self): + """Test emit_error with whitespace-only error code.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, " ", "Test message", False) + + # Should use fallback error code + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["error"] == "UNKNOWN_ERROR" + + def test_emit_error_invalid_code_non_string(self): + """Test emit_error with non-string error code.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, 404, "Test message", False) + + # Should use fallback error code + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["error"] == "UNKNOWN_ERROR" + + def test_emit_error_invalid_message_none(self): + """Test emit_error with None message.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, "test_error", None, False) + + # Should use fallback message + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["error_message"] == "Unknown error occurred" + + def test_emit_error_invalid_message_empty_string(self): + """Test emit_error with empty string message.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, "test_error", "", False) + + # Empty string passes through (not converted to fallback) + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["error_message"] == "" + + def test_emit_error_invalid_message_non_string(self): + """Test emit_error with non-string message.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, "test_error", 404, False) + + # Should convert to string + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + assert parsed["error_message"] == "404" + + def test_emit_error_json_serialization_failure(self): + """Test emit_error when JSON serialization fails.""" + ctx = click.Context(click.Command("test")) + + # Create an object that can't be JSON serialized + class NonSerializable: + pass + + with ( + pytest.raises(click.exceptions.Exit), + patch("click.echo") as mock_echo, + patch("json.dumps", side_effect=TypeError("Not serializable")), + ): + emit_error(ctx, 123, "test_error", "Test message", False) + + # Should fall back to simple text output + mock_echo.assert_called_once_with( + "Error (code: test_error): Test message", err=True + ) + + def test_emit_error_comprehensive_fallbacks(self): + """Test emit_error with all invalid inputs to verify fallback behavior.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, "bad_pr", None, "", False) + + # Should use all fallbacks + call_args = mock_echo.call_args + json_output = call_args[0][0] + parsed = json.loads(json_output) + + assert parsed["pr_number"] == 0 + assert parsed["error"] == "UNKNOWN_ERROR" + assert parsed["error_message"] == "" # Empty string passes through + assert parsed["success"] is False + + @pytest.mark.parametrize( + "pretty,expected_calls", + [ + (True, 1), # Pretty format calls echo once + (False, 1), # JSON format calls echo once + ], + ) + def test_emit_error_call_counts(self, pretty, expected_calls): + """Test that emit_error makes expected number of echo calls.""" + ctx = click.Context(click.Command("test")) + + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, "test_error", "Test message", pretty) + + assert mock_echo.call_count == expected_calls + + +@pytest.mark.unit +class TestUtilsIntegration: + """Integration tests for utility functions.""" + + def test_parse_datetime_and_emit_error_integration(self): + """Test integration between parse_datetime and emit_error.""" + ctx = click.Context(click.Command("test")) + + # Test that parse_datetime errors can be handled by emit_error + try: + parse_datetime("invalid-date") + except ValidationError as e: + with pytest.raises(click.exceptions.Exit), patch("click.echo") as mock_echo: + emit_error(ctx, 123, "parse_error", str(e), True) + + mock_echo.assert_called_once() + call_args = mock_echo.call_args + assert call_args[1]["err"] is True + assert "Unable to parse datetime" in call_args[0][0] + + def test_parse_datetime_timezone_edge_cases(self): + """Test parse_datetime with complex timezone scenarios.""" + test_cases = [ + # Test date with multiple dashes but no timezone + ("2024-01-15T10:30:45", datetime(2024, 1, 15, 10, 30, 45)), + # Test date with timezone that has multiple colons + ("2024-01-15T10:30:45-05:00", datetime(2024, 1, 15, 10, 30, 45)), + # Test microseconds with Z + ("2024-01-15T10:30:45.123Z", datetime(2024, 1, 15, 10, 30, 45, 123000)), + ] + + for date_str, expected in test_cases: + result = parse_datetime(date_str) + assert result == expected + + def test_utils_module_constants_and_functions(self): + """Test that all expected module exports are available.""" + from toady import utils + + # Test constants + assert hasattr(utils, "MAX_PR_NUMBER") + assert utils.MAX_PR_NUMBER == 999999 + + # Test functions + assert hasattr(utils, "parse_datetime") + assert hasattr(utils, "emit_error") + assert callable(utils.parse_datetime) + assert callable(utils.emit_error) diff --git a/tests/unit/validators/test_node_id_validation.py b/tests/unit/validators/test_node_id_validation.py index 26e4562..c09c477 100644 --- a/tests/unit/validators/test_node_id_validation.py +++ b/tests/unit/validators/test_node_id_validation.py @@ -16,6 +16,8 @@ ) +@pytest.mark.validator +@pytest.mark.unit class TestGitHubEntityType: """Test the GitHubEntityType enumeration.""" @@ -41,6 +43,8 @@ def test_all_entity_types_unique(self): assert len(values) == len(set(values)) +@pytest.mark.validator +@pytest.mark.unit class TestNodeIDValidator: """Test the NodeIDValidator class.""" @@ -284,6 +288,8 @@ def test_format_allowed_types_message_without_numeric(self): assert "IC_kwDOABcD12MAAAABcDE3fg" in message +@pytest.mark.validator +@pytest.mark.unit class TestPreConfiguredValidators: """Test pre-configured validator factory functions.""" diff --git a/tests/unit/validators/test_schema_validator.py b/tests/unit/validators/test_schema_validator.py index fa15cd6..15c282a 100644 --- a/tests/unit/validators/test_schema_validator.py +++ b/tests/unit/validators/test_schema_validator.py @@ -1,9 +1,9 @@ """Tests for GraphQL schema validation functionality.""" -import json -import tempfile from datetime import datetime, timedelta +import json from pathlib import Path +import tempfile from unittest.mock import Mock import pytest @@ -642,3 +642,83 @@ def test_is_required_type(self, validator): # None assert validator._is_required_type(None) is False + + +class TestSchemaValidatorEdgeCases: + """Test edge cases for schema validator to improve coverage.""" + + def test_is_cache_valid_json_decode_error(self, tmp_path): + """Test cache validation with invalid JSON in metadata.""" + cache_dir = tmp_path / ".toady" + cache_dir.mkdir() + + # Create invalid JSON metadata file + metadata_file = GitHubSchemaValidator( + cache_dir=cache_dir + )._get_cache_metadata_path() + metadata_file.write_text("invalid json content") + + validator = GitHubSchemaValidator(cache_dir=cache_dir) + assert validator._is_cache_valid() is False + + def test_is_cache_valid_key_error(self, tmp_path): + """Test cache validation with missing timestamp key.""" + cache_dir = tmp_path / ".toady" + cache_dir.mkdir() + + # Create metadata file without timestamp key + metadata_file = GitHubSchemaValidator( + cache_dir=cache_dir + )._get_cache_metadata_path() + metadata_file.write_text('{"some_other_key": "value"}') + + validator = GitHubSchemaValidator(cache_dir=cache_dir) + assert validator._is_cache_valid() is False + + def test_is_cache_valid_value_error(self, tmp_path): + """Test cache validation with invalid timestamp format.""" + cache_dir = tmp_path / ".toady" + cache_dir.mkdir() + + # Create metadata file with invalid timestamp format + metadata_file = GitHubSchemaValidator( + cache_dir=cache_dir + )._get_cache_metadata_path() + metadata_file.write_text('{"timestamp": "invalid-timestamp-format"}') + + validator = GitHubSchemaValidator(cache_dir=cache_dir) + assert validator._is_cache_valid() is False + + def test_load_cached_schema_not_exists(self, tmp_path): + """Test loading cached schema when file doesn't exist.""" + cache_dir = tmp_path / ".toady" + cache_dir.mkdir() + + validator = GitHubSchemaValidator(cache_dir=cache_dir) + # Create valid metadata but no schema file + metadata_file = cache_dir / "schema_metadata.json" + metadata_file.write_text(f'{{"timestamp": "{datetime.now().isoformat()}"}}') + + result = validator._load_cached_schema() + assert result is None + + def test_load_cached_schema_invalid_json(self, tmp_path): + """Test loading cached schema with invalid JSON.""" + cache_dir = tmp_path / ".toady" + cache_dir.mkdir() + + # Create valid metadata + metadata_file = cache_dir / "schema_metadata.json" + metadata_file.write_text(f'{{"timestamp": "{datetime.now().isoformat()}"}}') + + # Create invalid JSON schema file + schema_file = cache_dir / "github_schema.json" + schema_file.write_text("invalid json content") + + validator = GitHubSchemaValidator(cache_dir=cache_dir) + + from unittest.mock import patch + + with patch.object(validator, "_is_cache_valid", return_value=True): + result = validator._load_cached_schema() + assert result is None diff --git a/tests/unit/validators/test_validation.py b/tests/unit/validators/test_validation.py new file mode 100644 index 0000000..2b8c763 --- /dev/null +++ b/tests/unit/validators/test_validation.py @@ -0,0 +1,2009 @@ +"""Unit tests for the validation module (src/toady/validators/validation.py). + +This module provides comprehensive unit tests for all validation functions, +including parameter validation, data validation, error handling, edge cases, +boundary conditions, and warning systems. +""" + +from datetime import datetime +from unittest.mock import Mock, patch + +import pytest + +from toady.exceptions import ValidationError +from toady.validators.validation import ( + EMAIL_REGEX, + MAX_LIMIT_VALUE, + MAX_REPLY_BODY_LENGTH, + MIN_LIMIT_VALUE, + MIN_MEANINGFUL_CONTENT_LENGTH, + MIN_REPLY_BODY_LENGTH, + PLACEHOLDER_PATTERNS, + URL_REGEX, + USERNAME_REGEX, + ResolveOptions, + validate_boolean_flag, + validate_choice, + validate_comment_id, + validate_datetime_string, + validate_dict_keys, + validate_email, + validate_fetch_command_args, + validate_limit, + validate_non_empty_string, + validate_pr_number, + validate_reply_body, + validate_reply_command_args, + validate_reply_content_warnings, + validate_resolve_command_args, + validate_thread_id, + validate_url, + validate_username, +) + + +class TestConstants: + """Test validation constants are properly defined.""" + + def test_constants_exist(self): + """Test that all validation constants are defined.""" + assert MIN_REPLY_BODY_LENGTH == 3 + assert MAX_REPLY_BODY_LENGTH == 65536 + assert MIN_MEANINGFUL_CONTENT_LENGTH == 3 + assert MAX_LIMIT_VALUE == 1000 + assert MIN_LIMIT_VALUE == 1 + + def test_regex_patterns_compile(self): + """Test that all regex patterns compile correctly.""" + assert EMAIL_REGEX.pattern is not None + assert URL_REGEX.pattern is not None + assert USERNAME_REGEX.pattern is not None + + # Test that they can be used for matching + assert EMAIL_REGEX.match("test@example.com") is not None + assert URL_REGEX.match("https://example.com") is not None + assert USERNAME_REGEX.match("testuser") is not None + + def test_placeholder_patterns_set(self): + """Test that placeholder patterns are properly defined.""" + assert isinstance(PLACEHOLDER_PATTERNS, set) + assert len(PLACEHOLDER_PATTERNS) > 0 + assert "test" in PLACEHOLDER_PATTERNS + assert "..." in PLACEHOLDER_PATTERNS + + +class TestValidatePRNumber: + """Test PR number validation with comprehensive coverage.""" + + def test_valid_pr_numbers_integers(self): + """Test valid PR numbers as integers.""" + assert validate_pr_number(1) == 1 + assert validate_pr_number(123) == 123 + assert validate_pr_number(999999) == 999999 + + def test_valid_pr_numbers_strings(self): + """Test valid PR numbers as strings.""" + assert validate_pr_number("1") == 1 + assert validate_pr_number("123") == 123 + assert validate_pr_number("999999") == 999999 + + def test_pr_number_string_whitespace_handling(self): + """Test PR number validation with whitespace.""" + assert validate_pr_number(" 123 ") == 123 + assert validate_pr_number("\t456\n") == 456 + assert validate_pr_number(" \t 789 \n ") == 789 + + def test_pr_number_none_values(self): + """Test None handling with allow_none parameter.""" + assert validate_pr_number(None, allow_none=True) is None + + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "PR number" + + def test_pr_number_none_with_custom_field_name(self): + """Test None handling with custom field name.""" + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(None, field_name="Custom PR") + error = exc_info.value + assert "Custom PR cannot be None" in error.message + assert error.field_name == "Custom PR" + + def test_pr_number_zero_and_negative(self): + """Test zero and negative PR numbers.""" + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(0) + assert "must be positive" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(-1) + assert "must be positive" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(-999) + assert "must be positive" in str(exc_info.value) + + def test_pr_number_too_large(self): + """Test PR numbers that are too large.""" + from toady.utils import MAX_PR_NUMBER + + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(MAX_PR_NUMBER + 1) + assert "unreasonably large" in str(exc_info.value) + + def test_pr_number_empty_string(self): + """Test empty string handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_pr_number("") + assert "cannot be empty" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(" ") + assert "cannot be empty" in str(exc_info.value) + + def test_pr_number_non_numeric_strings(self): + """Test non-numeric string handling.""" + invalid_values = ["abc", "12abc", "abc12", "12.34", "1.0", "not-a-number"] + + for invalid_value in invalid_values: + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(invalid_value) + assert "must be numeric" in str(exc_info.value) + + def test_pr_number_float_values(self): + """Test float value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(123.45) + assert "must be an integer" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(123.0) + assert "must be an integer" in str(exc_info.value) + + def test_pr_number_other_types(self): + """Test other invalid types.""" + invalid_types = [[], {}, set(), object(), lambda x: x] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(invalid_type) + assert "must be an integer" in str(exc_info.value) + + def test_pr_number_boundary_values(self): + """Test boundary values.""" + # Test minimum valid value + assert validate_pr_number(1) == 1 + + # Test just below maximum + from toady.utils import MAX_PR_NUMBER + + assert validate_pr_number(MAX_PR_NUMBER) == MAX_PR_NUMBER + assert validate_pr_number(MAX_PR_NUMBER - 1) == MAX_PR_NUMBER - 1 + + +class TestValidateCommentID: + """Test comment ID validation with comprehensive coverage.""" + + def test_valid_numeric_comment_ids(self): + """Test valid numeric comment IDs.""" + assert validate_comment_id("123456789") == "123456789" + assert validate_comment_id(123456789) == "123456789" + assert validate_comment_id("1") == "1" + + def test_valid_github_node_comment_ids(self): + """Test valid GitHub node comment IDs.""" + valid_comment_ids = [ + "IC_kwDOABcD12MAAAABcDE3fg", + "PRRC_kwDOABcD12MAAAABcDE3fg", + "RP_kwDOABcD12MAAAABcDE3fg", + ] + + for comment_id in valid_comment_ids: + assert validate_comment_id(comment_id) == comment_id + + def test_comment_id_whitespace_handling(self): + """Test comment ID validation with whitespace.""" + assert ( + validate_comment_id(" IC_kwDOABcD12MAAAABcDE3fg ") + == "IC_kwDOABcD12MAAAABcDE3fg" + ) + assert validate_comment_id("\t123456789\n") == "123456789" + + def test_comment_id_thread_ids_when_allowed(self): + """Test accepting thread IDs when allow_thread_ids=True.""" + thread_ids = [ + "PRT_kwDOABcD12MAAAABcDE3fg", + "PRRT_kwDOABcD12MAAAABcDE3fg", + "RT_kwDOABcD12MAAAABcDE3fg", + ] + + for thread_id in thread_ids: + result = validate_comment_id(thread_id, allow_thread_ids=True) + assert result == thread_id + + def test_comment_id_thread_ids_when_not_allowed(self): + """Test rejecting thread IDs when allow_thread_ids=False.""" + thread_ids = [ + "PRT_kwDOABcD12MAAAABcDE3fg", + "PRRT_kwDOABcD12MAAAABcDE3fg", + "RT_kwDOABcD12MAAAABcDE3fg", + ] + + for thread_id in thread_ids: + with pytest.raises(ValidationError): + validate_comment_id(thread_id, allow_thread_ids=False) + + def test_comment_id_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_comment_id(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Comment ID" + + def test_comment_id_empty_string(self): + """Test empty string handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_comment_id("") + assert "cannot be empty" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_comment_id(" ") + assert "cannot be empty" in str(exc_info.value) + + def test_comment_id_invalid_node_format(self): + """Test invalid node ID formats.""" + invalid_ids = [ + "INVALID_kwDOABcD12MAAAABcDE3fg", + "IC_", # Too short + "NotANodeID", + ] + + for invalid_id in invalid_ids: + with pytest.raises(ValidationError): + validate_comment_id(invalid_id) + + def test_comment_id_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_comment_id(None, field_name="Custom Comment ID") + error = exc_info.value + assert error.field_name == "Custom Comment ID" + assert "Custom Comment ID cannot be None" in error.message + + @patch("toady.validators.validation.create_comment_validator") + def test_comment_id_validator_integration(self, mock_create_validator): + """Test integration with node ID validator.""" + mock_validator = Mock() + mock_create_validator.return_value = mock_validator + + validate_comment_id("IC_test123") + + mock_create_validator.assert_called_once() + mock_validator.validate_id.assert_called_once_with("IC_test123", "Comment ID") + + @patch("toady.validators.validation.create_universal_validator") + def test_comment_id_universal_validator_when_thread_ids_allowed( + self, mock_create_validator + ): + """Test integration with universal validator when thread IDs are allowed.""" + mock_validator = Mock() + mock_create_validator.return_value = mock_validator + + validate_comment_id("IC_test123", allow_thread_ids=True) + + mock_create_validator.assert_called_once() + mock_validator.validate_id.assert_called_once_with("IC_test123", "Comment ID") + + +class TestValidateThreadID: + """Test thread ID validation with comprehensive coverage.""" + + def test_valid_numeric_thread_ids(self): + """Test valid numeric thread IDs.""" + assert validate_thread_id("123456789") == "123456789" + assert validate_thread_id(123456789) == "123456789" + assert validate_thread_id("1") == "1" + + def test_valid_github_node_thread_ids(self): + """Test valid GitHub node thread IDs.""" + valid_thread_ids = [ + "PRT_kwDOABcD12MAAAABcDE3fg", + "PRRT_kwDOABcD12MAAAABcDE3fg", + "RT_kwDOABcD12MAAAABcDE3fg", + ] + + for thread_id in valid_thread_ids: + assert validate_thread_id(thread_id) == thread_id + + def test_thread_id_whitespace_handling(self): + """Test thread ID validation with whitespace.""" + assert ( + validate_thread_id(" PRT_kwDOABcD12MAAAABcDE3fg ") + == "PRT_kwDOABcD12MAAAABcDE3fg" + ) + assert validate_thread_id("\t123456789\n") == "123456789" + + def test_thread_id_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_thread_id(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Thread ID" + + def test_thread_id_empty_string(self): + """Test empty string handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_thread_id("") + assert "cannot be empty" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_thread_id(" ") + assert "cannot be empty" in str(exc_info.value) + + def test_thread_id_comment_ids_rejected(self): + """Test that comment IDs are rejected.""" + comment_ids = [ + "IC_kwDOABcD12MAAAABcDE3fg", + "PRRC_kwDOABcD12MAAAABcDE3fg", + "RP_kwDOABcD12MAAAABcDE3fg", + ] + + for comment_id in comment_ids: + with pytest.raises(ValidationError): + validate_thread_id(comment_id) + + def test_thread_id_invalid_node_format(self): + """Test invalid node ID formats.""" + invalid_ids = [ + "INVALID_kwDOABcD12MAAAABcDE3fg", + "PRT_", # Too short + "NotANodeID", + ] + + for invalid_id in invalid_ids: + with pytest.raises(ValidationError): + validate_thread_id(invalid_id) + + def test_thread_id_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_thread_id(None, field_name="Custom Thread ID") + error = exc_info.value + assert error.field_name == "Custom Thread ID" + assert "Custom Thread ID cannot be None" in error.message + + @patch("toady.validators.validation.create_thread_validator") + def test_thread_id_validator_integration(self, mock_create_validator): + """Test integration with thread ID validator.""" + mock_validator = Mock() + mock_create_validator.return_value = mock_validator + + validate_thread_id("PRT_test123") + + mock_create_validator.assert_called_once() + mock_validator.validate_id.assert_called_once_with("PRT_test123", "Thread ID") + + +class TestValidateReplyBody: + """Test reply body validation with comprehensive coverage.""" + + def test_valid_reply_bodies(self): + """Test valid reply bodies.""" + valid_bodies = [ + "This is a valid reply", + "Short but meaningful", + "A" * MAX_REPLY_BODY_LENGTH, # Maximum length + "This has some\nnewlines\nand\ttabs", + "Valid reply with @mention in middle", + "123 numbers are fine too", + "Special chars: !@#$%^&*()_+-=[]{}|;':\",./<>?", + ] + + for body in valid_bodies: + result = validate_reply_body(body) + assert result == body.strip() + + def test_reply_body_whitespace_trimming(self): + """Test reply body whitespace trimming.""" + assert ( + validate_reply_body(" This is a valid reply ") == "This is a valid reply" + ) + assert validate_reply_body("\t\nValid reply\n\t") == "Valid reply" + + def test_reply_body_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Reply body" + + def test_reply_body_non_string_types(self): + """Test non-string type handling.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(invalid_type) + assert "must be a string" in str(exc_info.value) + + def test_reply_body_empty_after_stripping(self): + """Test empty string after stripping.""" + empty_values = ["", " ", "\t\n\r", " \t\n "] + + for empty_value in empty_values: + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(empty_value) + assert "cannot be empty" in str(exc_info.value) + + def test_reply_body_too_short(self): + """Test reply bodies that are too short.""" + short_bodies = ["a", "ab", " a ", "\tab\t"] + + for short_body in short_bodies: + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(short_body) + assert "must be at least 3 characters" in str(exc_info.value) + + def test_reply_body_too_long(self): + """Test reply bodies that are too long.""" + too_long_body = "A" * (MAX_REPLY_BODY_LENGTH + 1) + + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(too_long_body) + assert "cannot exceed 65,536 characters" in str(exc_info.value) + assert "GitHub limit" in str(exc_info.value) + + def test_reply_body_insufficient_meaningful_content(self): + """Test replies with insufficient meaningful content.""" + # Content with less than MIN_MEANINGFUL_CONTENT_LENGTH non-whitespace chars + insufficient_content = [ + "a \n\t b", # Only 2 non-whitespace chars + " x y ", # Only 2 non-whitespace chars + "a \t\n b", # Only 2 non-whitespace chars when meaningful calculated + ] + + for content in insufficient_content: + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(content) + # The error can be either length or meaningful content + # depending on which fails first + error_msg = str(exc_info.value) + assert ( + "non-whitespace characters" in error_msg + or "must be at least 3 characters" in error_msg + ) + + def test_reply_body_placeholder_patterns(self): + """Test detection of placeholder patterns.""" + # Test patterns that are long enough but are placeholders + placeholder_bodies = ["...", "????", "test", "testing", "placeholder"] + + for placeholder in placeholder_bodies: + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(placeholder) + assert "placeholder text" in str(exc_info.value) + + def test_reply_body_custom_length_constraints(self): + """Test custom length constraints.""" + # Test with custom min_length + assert validate_reply_body("hello", min_length=5) == "hello" + + with pytest.raises(ValidationError) as exc_info: + validate_reply_body("hi", min_length=5) + assert "must be at least 5 characters" in str(exc_info.value) + + # Test with custom max_length + assert validate_reply_body("hello", max_length=10) == "hello" + + with pytest.raises(ValidationError) as exc_info: + validate_reply_body("this is too long", max_length=10) + assert "cannot exceed 10 characters" in str(exc_info.value) + + def test_reply_body_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_reply_body(None, field_name="Custom Body") + error = exc_info.value + assert error.field_name == "Custom Body" + assert "Custom Body cannot be None" in error.message + + def test_reply_body_boundary_lengths(self): + """Test boundary length conditions.""" + # Test minimum valid length + min_valid = "a" * MIN_REPLY_BODY_LENGTH + assert validate_reply_body(min_valid) == min_valid + + # Test maximum valid length + max_valid = "a" * MAX_REPLY_BODY_LENGTH + assert validate_reply_body(max_valid) == max_valid + + +class TestValidateLimit: + """Test limit validation with comprehensive coverage.""" + + def test_valid_limits_integers(self): + """Test valid limit values as integers.""" + assert validate_limit(1) == 1 + assert validate_limit(100) == 100 + assert validate_limit(MAX_LIMIT_VALUE) == MAX_LIMIT_VALUE + + def test_valid_limits_strings(self): + """Test valid limit values as strings.""" + assert validate_limit("1") == 1 + assert validate_limit("100") == 100 + assert validate_limit(str(MAX_LIMIT_VALUE)) == MAX_LIMIT_VALUE + + def test_limit_whitespace_handling(self): + """Test limit validation with whitespace.""" + assert validate_limit(" 100 ") == 100 + assert validate_limit("\t456\n") == 456 + + def test_limit_custom_ranges(self): + """Test custom min/max ranges.""" + assert validate_limit(5, min_value=5, max_value=10) == 5 + assert validate_limit(10, min_value=5, max_value=10) == 10 + assert validate_limit(7, min_value=5, max_value=10) == 7 + + def test_limit_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_limit(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Limit" + + def test_limit_zero_and_negative(self): + """Test zero and negative limit values.""" + with pytest.raises(ValidationError) as exc_info: + validate_limit(0) + assert "must be at least 1" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_limit(-1) + assert "must be at least 1" in str(exc_info.value) + + def test_limit_too_large(self): + """Test limit values that are too large.""" + with pytest.raises(ValidationError) as exc_info: + validate_limit(MAX_LIMIT_VALUE + 1) + assert f"cannot exceed {MAX_LIMIT_VALUE}" in str(exc_info.value) + + def test_limit_empty_string(self): + """Test empty string handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_limit("") + assert "cannot be empty" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_limit(" ") + assert "cannot be empty" in str(exc_info.value) + + def test_limit_non_numeric_strings(self): + """Test non-numeric string handling.""" + invalid_values = ["abc", "12abc", "abc12", "12.34", "not-a-number"] + + for invalid_value in invalid_values: + with pytest.raises(ValidationError) as exc_info: + validate_limit(invalid_value) + assert "must be numeric" in str(exc_info.value) + + def test_limit_other_types(self): + """Test other invalid types.""" + invalid_types = [[], {}, set(), object(), 123.45] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_limit(invalid_type) + assert "must be an integer" in str(exc_info.value) + + def test_limit_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_limit(None, field_name="Custom Limit") + error = exc_info.value + assert error.field_name == "Custom Limit" + + def test_limit_custom_range_validation(self): + """Test custom range validation.""" + with pytest.raises(ValidationError) as exc_info: + validate_limit(4, min_value=5, max_value=10) + assert "must be at least 5" in str(exc_info.value) + + with pytest.raises(ValidationError) as exc_info: + validate_limit(11, min_value=5, max_value=10) + assert "cannot exceed 10" in str(exc_info.value) + + +class TestValidateDatetimeString: + """Test datetime string validation with comprehensive coverage.""" + + def test_valid_datetime_strings(self): + """Test valid datetime strings.""" + valid_dates = [ + "2024-01-01T12:00:00", + "2024-12-31T23:59:59", + "2024-01-01T12:00:00.123456", + "2024-06-15T08:30:45", + ] + + for date_str in valid_dates: + result = validate_datetime_string(date_str) + assert isinstance(result, datetime) + + def test_datetime_string_whitespace_handling(self): + """Test datetime string with whitespace.""" + result = validate_datetime_string(" 2024-01-01T12:00:00 ") + assert isinstance(result, datetime) + assert result.year == 2024 + + def test_datetime_string_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Date" + + def test_datetime_string_non_string_types(self): + """Test non-string type handling.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string(invalid_type) + assert "must be a string" in str(exc_info.value) + + def test_datetime_string_empty_after_stripping(self): + """Test empty string after stripping.""" + empty_values = ["", " ", "\t\n\r"] + + for empty_value in empty_values: + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string(empty_value) + assert "cannot be empty" in str(exc_info.value) + + def test_datetime_string_invalid_formats(self): + """Test invalid datetime formats.""" + invalid_dates = [ + "not-a-date", + "2024-13-01T12:00:00", # Invalid month + "2024-01-32T12:00:00", # Invalid day + "2024-01-01T25:00:00", # Invalid hour + "2024-01-01", # Missing time + "12:00:00", # Missing date + "2024/01/01 12:00:00", # Wrong format + ] + + for invalid_date in invalid_dates: + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string(invalid_date) + assert "Invalid" in str(exc_info.value) + + def test_datetime_string_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string(None, field_name="Custom Date") + error = exc_info.value + assert error.field_name == "Custom Date" + + @patch("toady.utils.parse_datetime") + def test_datetime_string_parse_datetime_integration(self, mock_parse_datetime): + """Test integration with parse_datetime utility.""" + mock_parse_datetime.return_value = datetime(2024, 1, 1, 12, 0, 0) + + result = validate_datetime_string("2024-01-01T12:00:00") + + mock_parse_datetime.assert_called_once_with("2024-01-01T12:00:00") + assert result == datetime(2024, 1, 1, 12, 0, 0) + + @patch("toady.utils.parse_datetime") + def test_datetime_string_validation_error_from_parse_datetime( + self, mock_parse_datetime + ): + """Test handling of ValidationError from parse_datetime.""" + mock_parse_datetime.side_effect = ValidationError("Parse error") + + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string("invalid-date") + + assert "Invalid date format" in str(exc_info.value) + + @patch("toady.utils.parse_datetime") + def test_datetime_string_value_error_from_parse_datetime(self, mock_parse_datetime): + """Test handling of ValueError from parse_datetime.""" + mock_parse_datetime.side_effect = ValueError("Parse error message") + + with pytest.raises(ValidationError) as exc_info: + validate_datetime_string("invalid-date") + + assert "Invalid date format" in str(exc_info.value) + assert "Parse error message" in str(exc_info.value) + + +class TestValidateEmail: + """Test email validation with comprehensive coverage.""" + + def test_valid_emails(self): + """Test valid email addresses.""" + valid_emails = [ + "user@example.com", + "test.email+tag@domain.co.uk", + "user123@example.org", + "user-name@example-domain.com", + "user_name@example.com", + "123@example.com", + "a@b.co", + ] + + for email in valid_emails: + assert validate_email(email) == email + + def test_email_whitespace_handling(self): + """Test email validation with whitespace.""" + assert validate_email(" user@example.com ") == "user@example.com" + assert validate_email("\tuser@example.com\n") == "user@example.com" + + def test_email_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_email(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Email" + + def test_email_non_string_types(self): + """Test non-string type handling.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_email(invalid_type) + assert "must be a string" in str(exc_info.value) + + def test_email_empty_after_stripping(self): + """Test empty string after stripping.""" + empty_values = ["", " ", "\t\n\r"] + + for empty_value in empty_values: + with pytest.raises(ValidationError) as exc_info: + validate_email(empty_value) + assert "cannot be empty" in str(exc_info.value) + + def test_email_invalid_formats(self): + """Test invalid email formats.""" + invalid_emails = [ + "invalid", + "@example.com", + "user@", + "user.example.com", + "user@@example.com", + "user@.com", + "user@example.", + "user name@example.com", # Space in local part + "user@exam ple.com", # Space in domain + ] + + for invalid_email in invalid_emails: + with pytest.raises(ValidationError) as exc_info: + validate_email(invalid_email) + assert "Invalid email format" in str(exc_info.value) + + def test_email_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_email(None, field_name="User Email") + error = exc_info.value + assert error.field_name == "User Email" + + def test_email_regex_integration(self): + """Test integration with EMAIL_REGEX.""" + # Test that the function uses the EMAIL_REGEX pattern + valid_email = "test@example.com" + assert EMAIL_REGEX.match(valid_email) is not None + assert validate_email(valid_email) == valid_email + + +class TestValidateURL: + """Test URL validation with comprehensive coverage.""" + + def test_valid_urls(self): + """Test valid URLs.""" + valid_urls = [ + "https://example.com", + "http://example.com", + "https://example.com/path", + "https://example.com/path?query=1", + "https://example.com/path?query=1#fragment", + "https://sub.example.com", + "https://example.com:8080", + "https://example.com:8080/path?query=1#fragment", + ] + + for url in valid_urls: + assert validate_url(url) == url + + def test_url_whitespace_handling(self): + """Test URL validation with whitespace.""" + assert validate_url(" https://example.com ") == "https://example.com" + assert validate_url("\thttps://example.com\n") == "https://example.com" + + def test_url_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_url(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "URL" + + def test_url_non_string_types(self): + """Test non-string type handling.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_url(invalid_type) + assert "must be a string" in str(exc_info.value) + + def test_url_empty_after_stripping(self): + """Test empty string after stripping.""" + empty_values = ["", " ", "\t\n\r"] + + for empty_value in empty_values: + with pytest.raises(ValidationError) as exc_info: + validate_url(empty_value) + assert "cannot be empty" in str(exc_info.value) + + def test_url_invalid_formats(self): + """Test invalid URL formats.""" + invalid_urls = [ + "ftp://example.com", # Wrong protocol + "not-a-url", + "example.com", # Missing protocol + "https://", # Missing domain + "https:///path", # Missing domain + ] + + for invalid_url in invalid_urls: + with pytest.raises(ValidationError) as exc_info: + validate_url(invalid_url) + assert "Invalid url format" in str(exc_info.value) + + def test_url_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_url(None, field_name="Website URL") + error = exc_info.value + assert error.field_name == "Website URL" + + def test_url_regex_integration(self): + """Test integration with URL_REGEX.""" + # Test that the function uses the URL_REGEX pattern + valid_url = "https://example.com" + assert URL_REGEX.match(valid_url) is not None + assert validate_url(valid_url) == valid_url + + +class TestValidateUsername: + """Test username validation with comprehensive coverage.""" + + def test_valid_usernames(self): + """Test valid GitHub usernames.""" + valid_usernames = [ + "user", + "user-name", + "user123", + "123user", + "a", + "a" * 39, # Maximum length + "user-123", + "test-user-name", + ] + + for username in valid_usernames: + assert validate_username(username) == username + + def test_username_whitespace_handling(self): + """Test username validation with whitespace.""" + assert validate_username(" user ") == "user" + assert validate_username("\tuser123\n") == "user123" + + def test_username_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_username(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Username" + + def test_username_non_string_types(self): + """Test non-string type handling.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_username(invalid_type) + assert "must be a string" in str(exc_info.value) + + def test_username_empty_after_stripping(self): + """Test empty string after stripping.""" + empty_values = ["", " ", "\t\n\r"] + + for empty_value in empty_values: + with pytest.raises(ValidationError) as exc_info: + validate_username(empty_value) + assert "cannot be empty" in str(exc_info.value) + + def test_username_invalid_formats(self): + """Test invalid username formats.""" + invalid_usernames = [ + "-user", # Starts with hyphen + "user-", # Ends with hyphen + "user..name", # Double dot + "user.name", # Contains dot + "a" * 40, # Too long + "user name", # Contains space + "user@name", # Contains @ + "user#name", # Contains # + ] + + for invalid_username in invalid_usernames: + with pytest.raises(ValidationError) as exc_info: + validate_username(invalid_username) + assert "Invalid username format" in str(exc_info.value) + + def test_username_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_username(None, field_name="GitHub User") + error = exc_info.value + assert error.field_name == "GitHub User" + + def test_username_regex_integration(self): + """Test integration with USERNAME_REGEX.""" + # Test that the function uses the USERNAME_REGEX pattern + valid_username = "test-user123" + assert USERNAME_REGEX.match(valid_username) is not None + assert validate_username(valid_username) == valid_username + + +class TestValidateNonEmptyString: + """Test non-empty string validation with comprehensive coverage.""" + + def test_valid_non_empty_strings(self): + """Test valid non-empty strings.""" + valid_strings = [ + "hello", + "a", + "test string", + "123", + "!@#$%^&*()", + ] + + for string in valid_strings: + assert validate_non_empty_string(string) == string + + def test_non_empty_string_whitespace_handling(self): + """Test non-empty string with whitespace.""" + assert validate_non_empty_string(" hello ") == "hello" + assert validate_non_empty_string("\ttest\n") == "test" + + def test_non_empty_string_custom_length_constraints(self): + """Test custom length constraints.""" + assert validate_non_empty_string("hello", min_length=5) == "hello" + assert validate_non_empty_string("hello", max_length=10) == "hello" + assert ( + validate_non_empty_string("hello", min_length=3, max_length=10) == "hello" + ) + + def test_non_empty_string_none_value(self): + """Test None value handling.""" + with pytest.raises(ValidationError) as exc_info: + validate_non_empty_string(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Value" + + def test_non_empty_string_non_string_types(self): + """Test non-string type handling.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_non_empty_string(invalid_type) + assert "must be a string" in str(exc_info.value) + + def test_non_empty_string_empty_after_stripping(self): + """Test empty string after stripping.""" + empty_values = ["", " ", "\t\n\r"] + + for empty_value in empty_values: + with pytest.raises(ValidationError) as exc_info: + validate_non_empty_string(empty_value) + assert "cannot be empty" in str(exc_info.value) + + def test_non_empty_string_too_short(self): + """Test strings that are too short.""" + with pytest.raises(ValidationError) as exc_info: + validate_non_empty_string("ab", min_length=3) + assert "must be at least 3 characters" in str(exc_info.value) + + def test_non_empty_string_too_long(self): + """Test strings that are too long.""" + with pytest.raises(ValidationError) as exc_info: + validate_non_empty_string("toolong", max_length=5) + assert "cannot exceed 5 characters" in str(exc_info.value) + + def test_non_empty_string_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_non_empty_string(None, field_name="Custom Field") + error = exc_info.value + assert error.field_name == "Custom Field" + + def test_non_empty_string_boundary_lengths(self): + """Test boundary length conditions.""" + # Test minimum valid length with custom constraint + assert validate_non_empty_string("abc", min_length=3) == "abc" + + # Test maximum valid length with custom constraint + assert validate_non_empty_string("abcde", max_length=5) == "abcde" + + +class TestValidateBooleanFlag: + """Test boolean flag validation with comprehensive coverage.""" + + def test_valid_boolean_values(self): + """Test valid boolean values.""" + assert validate_boolean_flag(True) is True + assert validate_boolean_flag(False) is False + + def test_valid_string_boolean_representations(self): + """Test valid string boolean representations.""" + true_values = [ + "true", + "TRUE", + "True", + "1", + "yes", + "YES", + "Yes", + "on", + "ON", + "On", + ] + false_values = [ + "false", + "FALSE", + "False", + "0", + "no", + "NO", + "No", + "off", + "OFF", + "Off", + ] + + for true_value in true_values: + assert validate_boolean_flag(true_value) is True + + for false_value in false_values: + assert validate_boolean_flag(false_value) is False + + def test_boolean_flag_string_whitespace_handling(self): + """Test boolean flag string with whitespace.""" + assert validate_boolean_flag(" true ") is True + assert validate_boolean_flag("\tfalse\n") is False + + def test_boolean_flag_none_value_with_allow_none(self): + """Test None value handling with allow_none=True.""" + assert validate_boolean_flag(None, allow_none=True) is False + + def test_boolean_flag_none_value_without_allow_none(self): + """Test None value handling with allow_none=False.""" + with pytest.raises(ValidationError) as exc_info: + validate_boolean_flag(None) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Flag" + + def test_boolean_flag_invalid_string_values(self): + """Test invalid string boolean values.""" + invalid_values = ["invalid", "maybe", "2", "-1", "TRUE_FALSE", "yes_no"] + + for invalid_value in invalid_values: + with pytest.raises(ValidationError) as exc_info: + validate_boolean_flag(invalid_value) + assert "must be a boolean value" in str(exc_info.value) + + def test_boolean_flag_invalid_types(self): + """Test invalid types for boolean flag.""" + invalid_types = [123, [], {}, set(), object()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_boolean_flag(invalid_type) + assert "must be a boolean value" in str(exc_info.value) + + def test_boolean_flag_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_boolean_flag(None, field_name="Custom Flag") + error = exc_info.value + assert error.field_name == "Custom Flag" + + +class TestValidateChoice: + """Test choice validation with comprehensive coverage.""" + + def test_valid_choices(self): + """Test valid choice values.""" + choices = ["option1", "option2", "option3"] + + for choice in choices: + assert validate_choice(choice, choices) == choice + + def test_choice_case_sensitive_matching(self): + """Test case-sensitive choice matching.""" + choices = ["Option1", "Option2"] + + assert validate_choice("Option1", choices) == "Option1" + + # Should fail with different case when case_sensitive=True + with pytest.raises(ValidationError): + validate_choice("option1", choices, case_sensitive=True) + + def test_choice_case_insensitive_matching(self): + """Test case-insensitive choice matching.""" + choices = ["Option1", "Option2"] + + assert validate_choice("option1", choices, case_sensitive=False) == "Option1" + assert validate_choice("OPTION2", choices, case_sensitive=False) == "Option2" + assert validate_choice("OpTiOn1", choices, case_sensitive=False) == "Option1" + + def test_choice_non_string_values(self): + """Test choice validation with non-string values.""" + choices = [1, 2, 3] + + assert validate_choice(1, choices) == 1 + assert validate_choice(2, choices) == 2 + + def test_choice_mixed_type_choices(self): + """Test choice validation with mixed type choices.""" + choices = ["string", 123, True] + + assert validate_choice("string", choices) == "string" + assert validate_choice(123, choices) == 123 + assert validate_choice(True, choices) is True + + def test_choice_none_value(self): + """Test None value handling.""" + choices = ["option1", "option2"] + + with pytest.raises(ValidationError) as exc_info: + validate_choice(None, choices) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Value" + + def test_choice_invalid_choice(self): + """Test invalid choice values.""" + choices = ["option1", "option2"] + + with pytest.raises(ValidationError) as exc_info: + validate_choice("invalid", choices) + assert "must be one of the allowed values" in str(exc_info.value) + + def test_choice_custom_field_name(self): + """Test custom field name in error messages.""" + choices = ["option1", "option2"] + + with pytest.raises(ValidationError) as exc_info: + validate_choice(None, choices, field_name="Custom Choice") + error = exc_info.value + assert error.field_name == "Custom Choice" + + def test_choice_error_message_includes_choices(self): + """Test that error messages include available choices.""" + choices = ["option1", "option2", "option3"] + + with pytest.raises(ValidationError) as exc_info: + validate_choice("invalid", choices) + + error = exc_info.value + # The choices are included in the expected_format field + for choice in choices: + assert str(choice) in error.expected_format + + +class TestValidateDictKeys: + """Test dictionary key validation with comprehensive coverage.""" + + def test_valid_dictionary_with_required_keys(self): + """Test valid dictionary with all required keys.""" + data = {"key1": "value1", "key2": "value2"} + required_keys = ["key1", "key2"] + + result = validate_dict_keys(data, required_keys) + assert result == data + + def test_valid_dictionary_with_optional_keys(self): + """Test valid dictionary with optional keys.""" + data = {"key1": "value1", "key2": "value2", "optional": "value"} + required_keys = ["key1", "key2"] + optional_keys = ["optional"] + + result = validate_dict_keys(data, required_keys, optional_keys) + assert result == data + + def test_valid_dictionary_with_extra_keys_allowed(self): + """Test valid dictionary with extra keys when optional_keys is None.""" + data = {"key1": "value1", "key2": "value2", "extra": "value"} + required_keys = ["key1", "key2"] + + # When optional_keys is None, extra keys are allowed + result = validate_dict_keys(data, required_keys, optional_keys=None) + assert result == data + + def test_dictionary_none_value(self): + """Test None value handling.""" + required_keys = ["key1", "key2"] + + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(None, required_keys) + error = exc_info.value + assert "cannot be None" in error.message + assert error.field_name == "Data" + + def test_dictionary_non_dict_types(self): + """Test non-dictionary type handling.""" + required_keys = ["key1", "key2"] + invalid_types = ["not-a-dict", 123, [], set()] + + for invalid_type in invalid_types: + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(invalid_type, required_keys) + assert "must be a dictionary" in str(exc_info.value) + + def test_dictionary_missing_required_keys(self): + """Test dictionary with missing required keys.""" + data = {"key1": "value1"} # Missing key2 + required_keys = ["key1", "key2"] + + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(data, required_keys) + + error_message = str(exc_info.value) + assert "Missing required keys" in error_message + assert "key2" in error_message + + def test_dictionary_multiple_missing_required_keys(self): + """Test dictionary with multiple missing required keys.""" + data = {"key1": "value1"} # Missing key2 and key3 + required_keys = ["key1", "key2", "key3"] + + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(data, required_keys) + + error_message = str(exc_info.value) + assert "Missing required keys" in error_message + assert "key2" in error_message + assert "key3" in error_message + + def test_dictionary_unexpected_keys(self): + """Test dictionary with unexpected keys.""" + data = {"key1": "value1", "key2": "value2", "unexpected": "value"} + required_keys = ["key1", "key2"] + optional_keys = [] # No optional keys allowed + + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(data, required_keys, optional_keys) + + error_message = str(exc_info.value) + assert "Unexpected keys" in error_message + assert "unexpected" in error_message + + def test_dictionary_multiple_unexpected_keys(self): + """Test dictionary with multiple unexpected keys.""" + data = { + "key1": "value1", + "key2": "value2", + "unexpected1": "value", + "unexpected2": "value", + } + required_keys = ["key1", "key2"] + optional_keys = [] + + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(data, required_keys, optional_keys) + + error_message = str(exc_info.value) + assert "Unexpected keys" in error_message + assert "unexpected1" in error_message + assert "unexpected2" in error_message + + def test_dictionary_custom_field_name(self): + """Test custom field name in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_dict_keys(None, ["key1"], field_name="Configuration") + error = exc_info.value + assert error.field_name == "Configuration" + + def test_dictionary_empty_required_keys(self): + """Test dictionary with empty required keys list.""" + data = {"key1": "value1"} + required_keys = [] + + result = validate_dict_keys(data, required_keys) + assert result == data + + def test_dictionary_empty_optional_keys(self): + """Test dictionary with empty optional keys list.""" + data = {"key1": "value1"} + required_keys = ["key1"] + optional_keys = [] + + result = validate_dict_keys(data, required_keys, optional_keys) + assert result == data + + +class TestValidateReplyContentWarnings: + """Test reply content warning system with comprehensive coverage.""" + + def test_no_warnings_for_normal_content(self): + """Test that normal content produces no warnings.""" + normal_content = [ + "This is a normal reply", + "Here's some feedback on your code", + "Looks good to me!", + "Could you please update the documentation?", + ] + + for content in normal_content: + warnings = validate_reply_content_warnings(content) + assert warnings == [] + + def test_mention_warning(self): + """Test mention warning detection.""" + mention_content = [ + "@user this is a mention", + "@someone can you help?", + "@dev-team please review", + ] + + for content in mention_content: + warnings = validate_reply_content_warnings(content) + assert len(warnings) == 1 + assert "mention users" in warnings[0] + + def test_no_mention_warning_for_mention_in_middle(self): + """Test that mentions in the middle don't trigger warning.""" + non_mention_content = [ + "This reply mentions @user in the middle", + "Contact support at support@company.com", + "The email address is user@example.com", + ] + + for content in non_mention_content: + warnings = validate_reply_content_warnings(content) + mention_warnings = [w for w in warnings if "mention" in w] + assert len(mention_warnings) == 0 + + def test_repetitive_content_warning(self): + """Test repetitive content warning detection.""" + repetitive_content = [ + "aaaaaaaaaaaaaaaa", # Same character repeated + "abababababababab", # Same pattern repeated + "xxxxxxxxxxxxxxxxxxx", # Long repetition + ] + + for content in repetitive_content: + warnings = validate_reply_content_warnings(content) + repetitive_warnings = [w for w in warnings if "repetitive" in w] + assert len(repetitive_warnings) == 1 + + def test_no_repetitive_warning_for_short_content(self): + """Test that short content doesn't trigger repetitive warning.""" + short_content = [ + "aaaaaaa", # Short repetition + "abcdef", # Normal short content + ] + + for content in short_content: + warnings = validate_reply_content_warnings(content) + repetitive_warnings = [w for w in warnings if "repetitive" in w] + assert len(repetitive_warnings) == 0 + + def test_all_caps_warning(self): + """Test all caps warning detection.""" + all_caps_content = [ + "THIS IS ALL CAPS CONTENT", + "WHY ARE YOU SHOUTING?", + "PLEASE FIX THIS IMMEDIATELY!", + ] + + for content in all_caps_content: + warnings = validate_reply_content_warnings(content) + caps_warnings = [w for w in warnings if "ALL CAPS" in w] + assert len(caps_warnings) == 1 + + def test_no_all_caps_warning_for_short_content(self): + """Test that short all caps content doesn't trigger warning.""" + short_caps_content = [ + "OK", + "YES", + "NO", + "API", + "HTTP", + ] + + for content in short_caps_content: + warnings = validate_reply_content_warnings(content) + caps_warnings = [w for w in warnings if "ALL CAPS" in w] + assert len(caps_warnings) == 0 + + def test_excessive_punctuation_warning(self): + """Test excessive punctuation warning detection.""" + excessive_punctuation_content = [ + "What?!?!?! Why?!?!?!", # Excessive exclamation and question marks + "This is crazy!!!!!!", # Too many exclamation marks + "Are you sure??????", # Too many question marks + ] + + for content in excessive_punctuation_content: + warnings = validate_reply_content_warnings(content) + punct_warnings = [w for w in warnings if "excessive punctuation" in w] + assert len(punct_warnings) == 1 + + def test_no_excessive_punctuation_warning_for_normal_use(self): + """Test that normal punctuation doesn't trigger warning.""" + normal_punctuation_content = [ + "What? Why!", # Normal use + "This is great!", # Single exclamation + "Are you sure?", # Single question mark + "Really?! That's amazing!", # Mixed but reasonable + ] + + for content in normal_punctuation_content: + warnings = validate_reply_content_warnings(content) + punct_warnings = [w for w in warnings if "excessive punctuation" in w] + assert len(punct_warnings) == 0 + + def test_multiple_warnings(self): + """Test content that triggers multiple warnings.""" + problematic_content = "@USER WHAT?!?!?! WHY?!?!?!" + + warnings = validate_reply_content_warnings(problematic_content) + + # Should have mention, all caps, and excessive punctuation warnings + assert len(warnings) == 3 + + warning_text = " ".join(warnings) + assert "mention users" in warning_text + assert "ALL CAPS" in warning_text + assert "excessive punctuation" in warning_text + + def test_warning_combinations(self): + """Test various combinations of warnings.""" + test_cases = [ + ("@USER THIS IS CAPS", ["mention", "ALL CAPS"]), + ("@user !!!!!!", ["mention", "excessive punctuation"]), + ("AAAAAAAAAAAAAAAA", ["repetitive", "ALL CAPS"]), + ("THIS IS CAPS!!!!!!", ["ALL CAPS", "excessive punctuation"]), + ("aaaaaaaaaaaaa!!!!!!", ["repetitive", "excessive punctuation"]), + ] + + for content, expected_warning_types in test_cases: + warnings = validate_reply_content_warnings(content) + warning_text = " ".join(warnings).lower() + + assert len(warnings) == len(expected_warning_types) + for expected_type in expected_warning_types: + assert expected_type.lower() in warning_text + + def test_edge_cases_for_warnings(self): + """Test edge cases for warning detection.""" + edge_cases = [ + ("CAPS", 0), # Short caps, shouldn't warn + ("!", 0), # Single punctuation + ("aaaaaaaaa", 0), # Just under the repetition threshold + ("x@y", 0), # @ symbol in middle, not a mention + ] + + for content, expected_warning_count in edge_cases: + warnings = validate_reply_content_warnings(content) + assert len(warnings) == expected_warning_count + + +class TestCompositeValidationFunctions: + """Test composite validation functions for commands.""" + + def test_validate_fetch_command_args_valid(self): + """Test valid fetch command arguments.""" + result = validate_fetch_command_args( + pr_number=123, pretty=True, resolved=False, limit=50 + ) + + expected = {"pr_number": 123, "pretty": True, "resolved": False, "limit": 50} + assert result == expected + + def test_validate_fetch_command_args_string_pr_number(self): + """Test fetch command args with string PR number.""" + result = validate_fetch_command_args( + pr_number="456", pretty=False, resolved=True, limit="25" + ) + + expected = {"pr_number": 456, "pretty": False, "resolved": True, "limit": 25} + assert result == expected + + def test_validate_fetch_command_args_defaults(self): + """Test fetch command args with default values.""" + result = validate_fetch_command_args(pr_number=123) + + expected = {"pr_number": 123, "pretty": False, "resolved": False, "limit": 100} + assert result == expected + + def test_validate_fetch_command_args_invalid_pr_number(self): + """Test fetch command args with invalid PR number.""" + with pytest.raises(ValidationError): + validate_fetch_command_args(pr_number=-1) + + def test_validate_fetch_command_args_invalid_limit(self): + """Test fetch command args with invalid limit.""" + with pytest.raises(ValidationError): + validate_fetch_command_args(pr_number=123, limit=0) + + def test_validate_reply_command_args_valid(self): + """Test valid reply command arguments.""" + result = validate_reply_command_args( + comment_id="123456789", + body="This is a valid reply", + pretty=True, + verbose=False, + ) + + expected = { + "comment_id": "123456789", + "body": "This is a valid reply", + "pretty": True, + "verbose": False, + } + assert result == expected + + def test_validate_reply_command_args_node_id(self): + """Test reply command args with GitHub node ID.""" + result = validate_reply_command_args( + comment_id="IC_kwDOABcD12MAAAABcDE3fg", + body="Valid reply to node ID", + pretty=False, + verbose=True, + ) + + expected = { + "comment_id": "IC_kwDOABcD12MAAAABcDE3fg", + "body": "Valid reply to node ID", + "pretty": False, + "verbose": True, + } + assert result == expected + + def test_validate_reply_command_args_defaults(self): + """Test reply command args with default values.""" + result = validate_reply_command_args(comment_id="123456789", body="Valid reply") + + expected = { + "comment_id": "123456789", + "body": "Valid reply", + "pretty": False, + "verbose": False, + } + assert result == expected + + def test_validate_reply_command_args_allows_thread_ids(self): + """Test that reply command args allows thread IDs.""" + result = validate_reply_command_args( + comment_id="PRT_kwDOABcD12MAAAABcDE3fg", body="Reply to thread" + ) + + assert result["comment_id"] == "PRT_kwDOABcD12MAAAABcDE3fg" + + def test_validate_reply_command_args_invalid_comment_id(self): + """Test reply command args with invalid comment ID.""" + with pytest.raises(ValidationError): + validate_reply_command_args(comment_id=None, body="Valid reply") + + def test_validate_reply_command_args_invalid_body(self): + """Test reply command args with invalid body.""" + with pytest.raises(ValidationError): + validate_reply_command_args(comment_id="123456789", body="ab") # Too short + + +class TestResolveOptions: + """Test ResolveOptions dataclass.""" + + def test_resolve_options_defaults(self): + """Test ResolveOptions default values.""" + options = ResolveOptions() + + assert options.bulk_resolve is False + assert options.undo is False + assert options.yes is False + assert options.pretty is False + assert options.limit == 100 + + def test_resolve_options_custom_values(self): + """Test ResolveOptions with custom values.""" + options = ResolveOptions( + bulk_resolve=True, undo=True, yes=True, pretty=True, limit=50 + ) + + assert options.bulk_resolve is True + assert options.undo is True + assert options.yes is True + assert options.pretty is True + assert options.limit == 50 + + def test_resolve_options_partial_values(self): + """Test ResolveOptions with partial custom values.""" + options = ResolveOptions(bulk_resolve=True, limit=25) + + assert options.bulk_resolve is True + assert options.undo is False # Default + assert options.yes is False # Default + assert options.pretty is False # Default + assert options.limit == 25 + + +class TestValidateResolveCommandArgs: + """Test resolve command argument validation.""" + + def test_validate_resolve_command_args_single_thread(self): + """Test resolve command args for single thread resolution.""" + options = ResolveOptions( + bulk_resolve=False, undo=False, yes=False, pretty=True, limit=100 + ) + + result = validate_resolve_command_args( + thread_id="123456789", pr_number=None, options=options + ) + + expected = { + "thread_id": "123456789", + "bulk_resolve": False, + "pr_number": None, + "undo": False, + "yes": False, + "pretty": True, + "limit": 100, + } + assert result == expected + + def test_validate_resolve_command_args_bulk_operation(self): + """Test resolve command args for bulk operation.""" + options = ResolveOptions( + bulk_resolve=True, undo=False, yes=True, pretty=False, limit=50 + ) + + result = validate_resolve_command_args( + thread_id=None, pr_number=123, options=options + ) + + expected = { + "thread_id": None, + "bulk_resolve": True, + "pr_number": 123, + "undo": False, + "yes": True, + "pretty": False, + "limit": 50, + } + assert result == expected + + def test_validate_resolve_command_args_default_options(self): + """Test resolve command with default options (None).""" + result = validate_resolve_command_args( + thread_id="123456789", + pr_number=None, + options=None, # Should use default ResolveOptions + ) + + expected = { + "thread_id": "123456789", + "bulk_resolve": False, + "pr_number": None, + "undo": False, + "yes": False, + "pretty": False, + "limit": 100, + } + assert result == expected + + def test_validate_resolve_command_args_bulk_with_thread_id_conflict(self): + """Test resolve command args with bulk_resolve and thread_id conflict.""" + options = ResolveOptions(bulk_resolve=True) + + with pytest.raises(ValidationError) as exc_info: + validate_resolve_command_args( + thread_id="123456789", pr_number=123, options=options + ) + + assert "Cannot use bulk resolve and thread ID together" in str(exc_info.value) + + def test_validate_resolve_command_args_neither_bulk_nor_thread_id(self): + """Test resolve command args without bulk_resolve or thread_id.""" + options = ResolveOptions(bulk_resolve=False) + + with pytest.raises(ValidationError) as exc_info: + validate_resolve_command_args( + thread_id=None, pr_number=None, options=options + ) + + assert "Must specify either thread ID or bulk resolve" in str(exc_info.value) + + def test_validate_resolve_command_args_bulk_without_pr_number(self): + """Test resolve command args with bulk_resolve but no PR number.""" + options = ResolveOptions(bulk_resolve=True) + + with pytest.raises(ValidationError) as exc_info: + validate_resolve_command_args( + thread_id=None, pr_number=None, options=options + ) + + assert "PR number is required when using bulk resolve" in str(exc_info.value) + + def test_validate_resolve_command_args_invalid_thread_id(self): + """Test resolve command args with invalid thread ID.""" + options = ResolveOptions(bulk_resolve=False) + + with pytest.raises(ValidationError): + validate_resolve_command_args( + thread_id="INVALID_ID", pr_number=None, options=options + ) + + def test_validate_resolve_command_args_invalid_pr_number(self): + """Test resolve command args with invalid PR number.""" + options = ResolveOptions(bulk_resolve=True) + + with pytest.raises(ValidationError): + validate_resolve_command_args(thread_id=None, pr_number=-1, options=options) + + def test_validate_resolve_command_args_invalid_limit(self): + """Test resolve command args with invalid limit.""" + options = ResolveOptions(limit=0) + + with pytest.raises(ValidationError): + validate_resolve_command_args( + thread_id="123456789", pr_number=None, options=options + ) + + def test_validate_resolve_command_args_string_limit_in_options(self): + """Test resolve command args with string limit in options.""" + options = ResolveOptions(limit="50") + + result = validate_resolve_command_args( + thread_id="123456789", pr_number=None, options=options + ) + + assert result["limit"] == 50 + + +class TestValidationErrorHandling: + """Test validation error handling and messages.""" + + def test_validation_error_properties_from_pr_number(self): + """Test ValidationError properties from PR number validation.""" + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(-1) + + error = exc_info.value + assert error.field_name == "PR number" + assert error.invalid_value == -1 + assert error.expected_format == "positive integer" + assert "must be positive" in error.message + + def test_validation_error_properties_from_reply_body(self): + """Test ValidationError properties from reply body validation.""" + with pytest.raises(ValidationError) as exc_info: + validate_reply_body("ab") + + error = exc_info.value + assert error.field_name == "Reply body" + assert error.invalid_value == "ab" + assert "must be at least 3 characters" in error.message + + def test_custom_field_names_in_errors(self): + """Test custom field names in error messages.""" + with pytest.raises(ValidationError) as exc_info: + validate_pr_number(-1, field_name="Custom PR Field") + + error = exc_info.value + assert error.field_name == "Custom PR Field" + assert "Custom PR Field must be positive" in error.message + + def test_error_context_information_comprehensive(self): + """Test that validation errors include comprehensive context.""" + test_cases = [ + (lambda: validate_pr_number(None), ["cannot be None"], "PR number"), + (lambda: validate_comment_id(""), ["cannot be empty"], "Comment ID"), + (lambda: validate_limit(-1), ["must be at least 1"], "Limit"), + (lambda: validate_email("invalid"), ["Invalid email format"], "Email"), + (lambda: validate_url("not-a-url"), ["Invalid url format"], "URL"), + ] + + for test_func, expected_message_strings, expected_field_name in test_cases: + with pytest.raises(ValidationError) as exc_info: + test_func() + + error = exc_info.value + error_message = str(error) + for expected_string in expected_message_strings: + assert expected_string in error_message + assert error.field_name == expected_field_name + + def test_validation_error_field_name_consistency(self): + """Test that field names are consistent across similar validations.""" + field_name_tests = [ + (validate_pr_number, None, "PR number"), + (validate_comment_id, None, "Comment ID"), + (validate_thread_id, None, "Thread ID"), + (validate_reply_body, None, "Reply body"), + (validate_limit, None, "Limit"), + (validate_email, None, "Email"), + (validate_url, None, "URL"), + (validate_username, None, "Username"), + ] + + for validator_func, invalid_value, expected_field_name in field_name_tests: + with pytest.raises(ValidationError) as exc_info: + validator_func(invalid_value) + + error = exc_info.value + assert error.field_name == expected_field_name + + def test_validation_error_expected_format_specificity(self): + """Test that expected format messages are specific and helpful.""" + format_tests = [ + (lambda: validate_pr_number("abc"), "positive integer"), + ( + lambda: validate_limit("abc", min_value=1, max_value=100), + "integer between 1 and 100", + ), + (lambda: validate_email("invalid"), "valid email address"), + (lambda: validate_url("invalid"), "valid HTTP or HTTPS URL"), + (lambda: validate_username("invalid..name"), "valid GitHub username"), + ] + + for test_func, expected_format_substring in format_tests: + with pytest.raises(ValidationError) as exc_info: + test_func() + + error = exc_info.value + assert expected_format_substring in error.expected_format + + +class TestValidationIntegration: + """Test validation integration and comprehensive scenarios.""" + + def test_all_validators_handle_none_consistently(self): + """Test that all validators handle None values consistently.""" + validators_without_allow_none = [ + validate_comment_id, + validate_thread_id, + validate_reply_body, + validate_limit, + validate_datetime_string, + validate_email, + validate_url, + validate_username, + validate_non_empty_string, + validate_choice, + validate_dict_keys, + ] + + for validator in validators_without_allow_none: + with pytest.raises(ValidationError) as exc_info: + if validator == validate_choice: + validator(None, ["option1", "option2"]) + elif validator == validate_dict_keys: + validator(None, ["key1"]) + else: + validator(None) + + assert "cannot be None" in str(exc_info.value) + + def test_all_validators_have_consistent_error_structure(self): + """Test that all validators produce consistent error structures.""" + test_cases = [ + (validate_pr_number, None), + (validate_comment_id, None), + (validate_thread_id, None), + (validate_reply_body, None), + (validate_limit, None), + (validate_datetime_string, None), + (validate_email, None), + (validate_url, None), + (validate_username, None), + (validate_non_empty_string, None), + (validate_boolean_flag, None), + ] + + for validator, invalid_value in test_cases: + with pytest.raises(ValidationError) as exc_info: + validator(invalid_value) + + error = exc_info.value + assert hasattr(error, "field_name") + assert hasattr(error, "invalid_value") + assert hasattr(error, "expected_format") + assert hasattr(error, "message") + assert error.field_name is not None + assert error.message is not None + + def test_validation_with_realistic_github_data(self): + """Test validation with realistic GitHub data structures.""" + # Test realistic PR numbers + realistic_pr_numbers = [1, 42, 1337, 9999, 123456] + for pr_num in realistic_pr_numbers: + assert validate_pr_number(pr_num) == pr_num + + # Test realistic GitHub node IDs + realistic_comment_ids = [ + "IC_kwDOABcD12MAAAABcDE3fg", + "PRRC_kwDOBGHtJMAAAAB1234567", + "RP_kwDOABcD12MAAAABcDE3fh", + ] + for comment_id in realistic_comment_ids: + assert validate_comment_id(comment_id) == comment_id + + # Test realistic thread IDs + realistic_thread_ids = [ + "PRT_kwDOABcD12MAAAABcDE3fg", + "PRRT_kwDOBGHtJMAAAAB1234567", + "RT_kwDOABcD12MAAAABcDE3fh", + ] + for thread_id in realistic_thread_ids: + assert validate_thread_id(thread_id) == thread_id + + def test_validation_performance_with_large_inputs(self): + """Test validation performance with large inputs.""" + # Test with maximum length reply body + large_reply = "A" * MAX_REPLY_BODY_LENGTH + assert validate_reply_body(large_reply) == large_reply + + # Test with large dictionary + large_dict = {f"key_{i}": f"value_{i}" for i in range(100)} + required_keys = [f"key_{i}" for i in range(50)] + result = validate_dict_keys(large_dict, required_keys) + assert len(result) == 100 + + # Test with large choice list + large_choices = [f"option_{i}" for i in range(1000)] + assert validate_choice("option_500", large_choices) == "option_500" + + def test_validation_thread_safety_simulation(self): + """Test validation functions with concurrent-like usage patterns.""" + # Simulate concurrent validation of different data types + test_data = [ + (validate_pr_number, 123, 123), + (validate_comment_id, "IC_test", "IC_test"), + (validate_thread_id, "PRT_test", "PRT_test"), + (validate_reply_body, "Valid reply", "Valid reply"), + (validate_limit, 50, 50), + (validate_email, "test@example.com", "test@example.com"), + (validate_url, "https://example.com", "https://example.com"), + (validate_username, "testuser", "testuser"), + ] + + # Run multiple validation cycles + for _ in range(10): + for validator, input_val, expected in test_data: + if validator in [validate_comment_id, validate_thread_id]: + # Skip these as they require proper node ID validation + continue + result = validator(input_val) + assert result == expected diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..e0d5101 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1515 @@ +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 = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[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 = "black" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449, upload-time = "2025-01-29T04:15:40.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/3b/4ba3f93ac8d90410423fdd31d7541ada9bcee1df32fb90d26de41ed40e1d/black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32", size = 1629419, upload-time = "2025-01-29T05:37:06.642Z" }, + { url = "https://files.pythonhosted.org/packages/b4/02/0bde0485146a8a5e694daed47561785e8b77a0466ccc1f3e485d5ef2925e/black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da", size = 1461080, upload-time = "2025-01-29T05:37:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/abdf75183c830eaca7589144ff96d49bce73d7ec6ad12ef62185cc0f79a2/black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7", size = 1766886, upload-time = "2025-01-29T04:18:24.432Z" }, + { url = "https://files.pythonhosted.org/packages/dc/a6/97d8bb65b1d8a41f8a6736222ba0a334db7b7b77b8023ab4568288f23973/black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9", size = 1419404, upload-time = "2025-01-29T04:19:04.296Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/87f596aca05c3ce5b94b8663dbfe242a12843caaa82dd3f85f1ffdc3f177/black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0", size = 1614372, upload-time = "2025-01-29T05:37:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d0/2c34c36190b741c59c901e56ab7f6e54dad8df05a6272a9747ecef7c6036/black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299", size = 1442865, upload-time = "2025-01-29T05:37:14.309Z" }, + { url = "https://files.pythonhosted.org/packages/21/d4/7518c72262468430ead45cf22bd86c883a6448b9eb43672765d69a8f1248/black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096", size = 1749699, upload-time = "2025-01-29T04:18:17.688Z" }, + { url = "https://files.pythonhosted.org/packages/58/db/4f5beb989b547f79096e035c4981ceb36ac2b552d0ac5f2620e941501c99/black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2", size = 1428028, upload-time = "2025-01-29T04:18:51.711Z" }, + { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988, upload-time = "2025-01-29T05:37:16.707Z" }, + { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985, upload-time = "2025-01-29T05:37:18.273Z" }, + { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816, upload-time = "2025-01-29T04:18:33.823Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860, upload-time = "2025-01-29T04:19:12.944Z" }, + { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673, upload-time = "2025-01-29T05:37:20.574Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190, upload-time = "2025-01-29T05:37:22.106Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926, upload-time = "2025-01-29T04:18:58.564Z" }, + { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613, upload-time = "2025-01-29T04:19:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b6/ae7507470a4830dbbfe875c701e84a4a5fb9183d1497834871a715716a92/black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0", size = 1628593, upload-time = "2025-01-29T05:37:23.672Z" }, + { url = "https://files.pythonhosted.org/packages/24/c1/ae36fa59a59f9363017ed397750a0cd79a470490860bc7713967d89cdd31/black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f", size = 1460000, upload-time = "2025-01-29T05:37:25.829Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b6/98f832e7a6c49aa3a464760c67c7856363aa644f2f3c74cf7d624168607e/black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e", size = 1765963, upload-time = "2025-01-29T04:18:38.116Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e9/2cb0a017eb7024f70e0d2e9bdb8c5a5b078c5740c7f8816065d06f04c557/black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355", size = 1419419, upload-time = "2025-01-29T04:18:30.191Z" }, + { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646, upload-time = "2025-01-29T04:15:38.082Z" }, +] + +[[package]] +name = "build" +version = "1.2.2.post1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "os_name == 'nt'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/46/aeab111f8e06793e4f0e421fcad593d547fb8313b50990f31681ee2fb1ad/build-1.2.2.post1.tar.gz", hash = "sha256:b36993e92ca9375a219c99e606a122ff365a760a2d4bba0caa09bd5278b608b7", size = 46701, upload-time = "2024-10-06T17:22:25.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024, upload-time = "2024-09-04T20:43:34.186Z" }, + { url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188, upload-time = "2024-09-04T20:43:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571, upload-time = "2024-09-04T20:43:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687, upload-time = "2024-09-04T20:43:40.084Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211, upload-time = "2024-09-04T20:43:41.526Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325, upload-time = "2024-09-04T20:43:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784, upload-time = "2024-09-04T20:43:45.256Z" }, + { url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564, upload-time = "2024-09-04T20:43:46.779Z" }, + { url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" }, + { url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" }, + { url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/ed/65/25a8dc32c53bf5b7b6c2686b42ae2ad58743f7ff644844af7cdb29b49361/cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", size = 424910, upload-time = "2024-09-04T20:45:05.315Z" }, + { url = "https://files.pythonhosted.org/packages/42/7a/9d086fab7c66bd7c4d0f27c57a1b6b068ced810afc498cc8c49e0088661c/cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", size = 447200, upload-time = "2024-09-04T20:45:06.903Z" }, + { url = "https://files.pythonhosted.org/packages/da/63/1785ced118ce92a993b0ec9e0d0ac8dc3e5dbfbcaa81135be56c69cabbb6/cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", size = 454565, upload-time = "2024-09-04T20:45:08.975Z" }, + { url = "https://files.pythonhosted.org/packages/74/06/90b8a44abf3556599cdec107f7290277ae8901a58f75e6fe8f970cd72418/cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", size = 435635, upload-time = "2024-09-04T20:45:10.64Z" }, + { url = "https://files.pythonhosted.org/packages/bd/62/a1f468e5708a70b1d86ead5bab5520861d9c7eacce4a885ded9faa7729c3/cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", size = 445218, upload-time = "2024-09-04T20:45:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/5b/95/b34462f3ccb09c2594aa782d90a90b045de4ff1f70148ee79c69d37a0a5a/cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", size = 460486, upload-time = "2024-09-04T20:45:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/fc/fc/a1e4bebd8d680febd29cf6c8a40067182b64f00c7d105f8f26b5bc54317b/cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", size = 437911, upload-time = "2024-09-04T20:45:15.696Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c3/21cab7a6154b6a5ea330ae80de386e7665254835b9e98ecc1340b3a7de9a/cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", size = 460632, upload-time = "2024-09-04T20:45:17.284Z" }, +] + +[[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.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/d1/7b18a2e0d2994e4e108dadf16580ec192e0a9c65f7456ccb82ced059f9bf/coverage-7.9.0.tar.gz", hash = "sha256:1a93b43de2233a7670a8bf2520fed8ebd5eea6a65b47417500a9d882b0533fa2", size = 813385, upload-time = "2025-06-11T23:23:34.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/25/c83935ed228bd0ce277a9a92b505a4f67b0b15ba0344680974a77452c5dd/coverage-7.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3d494fa4256e3cb161ca1df14a91d2d703c27d60452eb0d4a58bb05f52f676e4", size = 211940, upload-time = "2025-06-11T23:21:47.353Z" }, + { url = "https://files.pythonhosted.org/packages/36/42/c58ca1fec2a346ad12356fac955a9b6d848ab37f632a7cb1bc7476efcf90/coverage-7.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b613efceeabf242978d14e1a65626ec3be67c5261918a82a985f56c2a05475ee", size = 212329, upload-time = "2025-06-11T23:21:50.216Z" }, + { url = "https://files.pythonhosted.org/packages/64/0a/6b61e4348cf7b0a70f7995247cde5cc4b5ef0b61d9718109896c77d9ed0e/coverage-7.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:673a4d2cb7ec78e1f2f6f41039f6785f27bca0f6bc0e722b53a58286d12754e1", size = 241447, upload-time = "2025-06-11T23:21:51.757Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1e/5f7060b909352cba70d34be0e34619659c0ddbef426665e036d5d3046b3c/coverage-7.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1edc2244932e9fed92ad14428b9480a97ecd37c970333688bd35048f6472f260", size = 239322, upload-time = "2025-06-11T23:21:53.826Z" }, + { url = "https://files.pythonhosted.org/packages/f5/78/f4ba669c9bf15b537136b663ccb846032cfb73e28b59458ef6899f18fe07/coverage-7.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8b92a7617faa2017bd44c94583830bab8be175722d420501680abc4f5bc794", size = 240467, upload-time = "2025-06-11T23:21:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/79/38/3246ea3ac68dc6f85afac0cb0362d3703647378b9882d55796c71fe83a1a/coverage-7.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d8f3ca1f128f11812d3baf0a482e7f36ffb856ac1ae14de3b5d1adcfb7af955d", size = 240376, upload-time = "2025-06-11T23:21:57.108Z" }, + { url = "https://files.pythonhosted.org/packages/c0/58/ef1f20afbaf9affe2941e7b077a8cf08075c6e3fe5e1dfc3160908b6a1de/coverage-7.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c30eed34eb8206d9b8c2d0d9fa342fa98e10f34b1e9e1eb05f79ccbf4499c8ff", size = 239046, upload-time = "2025-06-11T23:21:58.709Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/d510b05b3ca0da8fe746acf8ac815b2d560d6c4d5c4e0f6eafb2ec27dc33/coverage-7.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e6f8e5f125cd8bff33593a484a079305c9f0be911f76c6432f580ade5c1a17", size = 239318, upload-time = "2025-06-11T23:21:59.987Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/328a412e3bd78c049180df3f4374bb13a332ed8731ff66f49578d5ebf98c/coverage-7.9.0-cp310-cp310-win32.whl", hash = "sha256:a1b0317b4a8ff4d3703cd7aa642b4f963a71255abe4e878659f768238fab6602", size = 214430, upload-time = "2025-06-11T23:22:01.663Z" }, + { url = "https://files.pythonhosted.org/packages/db/a5/0e788cc4796989d77bfb6b1c58819edc2c65522926f0c08cfe42d1529f2b/coverage-7.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:512b1ea57a11dfa23b7f3d8fe8690fcf8cd983a70ae4c2c262cf5c972618fa15", size = 215350, upload-time = "2025-06-11T23:22:02.957Z" }, + { url = "https://files.pythonhosted.org/packages/9d/91/721a7df15263babfe89caf535a08bacbadebdef87338cf37d40f7400161b/coverage-7.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:55b7b9df45174956e0f719a56cf60c0cb4a7f155668881d00de6384e2a3402f4", size = 212055, upload-time = "2025-06-11T23:22:04.389Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d6/1f4c1eae67e698a8535ede02a6958a7587d06869d33a9b134ecc0e17ee07/coverage-7.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:87bceebbc91a58c9264c43638729fcb45910805b9f86444f93654d988305b3a2", size = 212445, upload-time = "2025-06-11T23:22:06.044Z" }, + { url = "https://files.pythonhosted.org/packages/bd/48/c375a6e6a266efa2d5fbf9b04eac88c87430d1a337b4f383ea8beeeedd44/coverage-7.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81da3b6e289bf9fc7dc159ab6d5222f5330ac6e94a6d06f147ba46e53fa6ec82", size = 245010, upload-time = "2025-06-11T23:22:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/7a/43/ec070ad02a1ee10837555a852b6fa256f8c71a953c209488e027673fc5b6/coverage-7.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b361684a91224d4362879c1b1802168d2435ff76666f1b7ba52fc300ad832dbc", size = 242725, upload-time = "2025-06-11T23:22:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ff/8b8efbd058dd59b489d9c5e27ba5766e895c396dd3bd1b78bebef9808c5f/coverage-7.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9a384ea4f77ac0a7e36c9a805ed95ef10f423bdb68b4e9487646cdf548a6a05", size = 244527, upload-time = "2025-06-11T23:22:10.416Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e7/3863f458a3af009a4817656f5b56fa90c7e363d73fef338601b275e979c4/coverage-7.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:38a5642aa82ea6de0e4331e346f5ba188a9fdb7d727e00199f55031b85135d0a", size = 244174, upload-time = "2025-06-11T23:22:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f0/2ff1fa06ccd3c3d653e352b10ddeec511b018890b28dbd3c29b6ea3f742e/coverage-7.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8c5ff4ca4890c0b57d3e80850534609493280c0f9e6ea2bd314b10cb8cbd76e0", size = 242227, upload-time = "2025-06-11T23:22:13.438Z" }, + { url = "https://files.pythonhosted.org/packages/32/e2/bae13555436f1d0278e70cfe22a0980eab9809e89361e859c96ffa788cb9/coverage-7.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cd052a0c4727ede06393da3c1df1ae6ef6c079e6bdfefb39079877404b3edc22", size = 242815, upload-time = "2025-06-11T23:22:14.723Z" }, + { url = "https://files.pythonhosted.org/packages/20/7c/e1b5b3313c1e3a5e8f8ced567fee67f18c8f18cebee8af0d69052f445a55/coverage-7.9.0-cp311-cp311-win32.whl", hash = "sha256:f73fd1128165e1d665cb7f863a91d00f073044a672c7dfa04ab400af4d1a9226", size = 214469, upload-time = "2025-06-11T23:22:16.187Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/0034d3ccbb7b8f80b1ce8a927ea06e2ba265bd0ba4a9a95a83026ac78dfd/coverage-7.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd62d62e782d3add529c8e7943f5600efd0d07dadf3819e5f9917edb4acf85d8", size = 215407, upload-time = "2025-06-11T23:22:17.611Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e1/7473bf679a43638c5ccba6228f45f68d33c3b7414ffae757dbb0bb2f1127/coverage-7.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:f75288785cc9a67aff3b04dafd8d0f0be67306018b224d319d23867a161578d6", size = 213778, upload-time = "2025-06-11T23:22:19.217Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6b/7bdef79e79076c7e3303ce2453072528ed13988210fb7a8702bb3d98ea8c/coverage-7.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:969ed1ed0ab0325b50af3204f9024782180e64fb281f5a2952f479ec60a02aba", size = 212252, upload-time = "2025-06-11T23:22:20.662Z" }, + { url = "https://files.pythonhosted.org/packages/08/fe/7e08dd50c3c3cfdbe822ee11e24da9f418983faefb4f5e52fbffae5beeb2/coverage-7.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1abd41781c874e716aaeecb8b27db5f4f2bc568f2ed8d41228aa087d567674f0", size = 212491, upload-time = "2025-06-11T23:22:22.002Z" }, + { url = "https://files.pythonhosted.org/packages/d4/65/9793cf61b3e4c5647e70aabd5b9470958ffd341c42f90730beeb4d21af9c/coverage-7.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eb6e99487dffd28c88a4fc2ea4286beaf0207a43388775900c93e56cc5a8ae3", size = 246294, upload-time = "2025-06-11T23:22:23.297Z" }, + { url = "https://files.pythonhosted.org/packages/2a/c9/fc61695132da06a34b27a49e853010a80d66a5534a1dfa770cb38aca71c0/coverage-7.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c425c85ddb62b32d44f83fb20044fe32edceceee1db1f978c062eec020a73ea5", size = 243311, upload-time = "2025-06-11T23:22:24.966Z" }, + { url = "https://files.pythonhosted.org/packages/62/0e/559a86887580d0de390e018bddfa632ae0762eeeb065bb5557f319071527/coverage-7.9.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0a1f7676bc90ceba67caa66850d689947d586f204ccf6478400c2bf39da5790", size = 245503, upload-time = "2025-06-11T23:22:26.316Z" }, + { url = "https://files.pythonhosted.org/packages/45/09/344d012dc91e60b8c7afee11ffae18338780c703a5b5fb32d8d82987e7cb/coverage-7.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f17055c50768d710d6abc789c9469d0353574780935e1381b83e63edc49ff530", size = 245313, upload-time = "2025-06-11T23:22:27.936Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2d/151b23e82aaea28aa7e3c0390d893bd1aef685866132aad36034f7d462b8/coverage-7.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:298d2917a6bfadbb272e08545ed026af3965e4d2fe71e3f38bf0a816818b226e", size = 243495, upload-time = "2025-06-11T23:22:29.72Z" }, + { url = "https://files.pythonhosted.org/packages/74/5c/0da7fd4ad44259b4b61bd429dc642c6511314a356ffa782b924bd1ea9e5c/coverage-7.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d9be5d26e5f817d478506e4d3c4ff7b92f17d980670b4791bf05baaa37ce2f88", size = 244727, upload-time = "2025-06-11T23:22:31.112Z" }, + { url = "https://files.pythonhosted.org/packages/de/08/6ccf2847c5c0d8fcc153bd8f4341d89ab50c85e01a15cabe4a546d3e943e/coverage-7.9.0-cp312-cp312-win32.whl", hash = "sha256:dc2784edd9ac9fe8692fc5505667deb0b05d895c016aaaf641031ed4a5f93d53", size = 214636, upload-time = "2025-06-11T23:22:33.257Z" }, + { url = "https://files.pythonhosted.org/packages/79/fa/ae2c14d49475215372772f7638c333deaaacda8f3c5717a75377d1992c82/coverage-7.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:18223198464a6d5549db1934cf77a15deb24bb88652c4f5f7cb21cd3ad853704", size = 215448, upload-time = "2025-06-11T23:22:35.125Z" }, + { url = "https://files.pythonhosted.org/packages/62/a9/45309219ba08b89cae84b2cb4ccfed8f941850aa7721c4914282fb3c1081/coverage-7.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:3b00194ff3c84d4b821822ff6c041f245fc55d0d5c7833fc4311d082e97595e8", size = 213817, upload-time = "2025-06-11T23:22:36.557Z" }, + { url = "https://files.pythonhosted.org/packages/0b/59/449eb05f795d0050007b57a4efee79b540fa6fcccad813a191351964a001/coverage-7.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:122c60e92ab66c9c88e17565f67a91b3b3be5617cb50f73cfd34a4c60ed4aab0", size = 212271, upload-time = "2025-06-11T23:22:38.305Z" }, + { url = "https://files.pythonhosted.org/packages/e0/3b/26852a4fb719a6007b0169c1b52116ed14b61267f0bf3ba1e23db516f352/coverage-7.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:813c11b367a6b3cf37212ec36b230f8d086c22b69dbf62877b40939fb2c79e74", size = 212538, upload-time = "2025-06-11T23:22:39.665Z" }, + { url = "https://files.pythonhosted.org/packages/f6/80/99f82896119f36984a5b9189e71c7310fc036613276560b5884b5ee890d7/coverage-7.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f05e0f5e87f23d43fefe49e86655c6209dd4f9f034786b983e6803cf4554183", size = 245705, upload-time = "2025-06-11T23:22:41.103Z" }, + { url = "https://files.pythonhosted.org/packages/a9/29/0b007deb096dd527c42e933129a8e4d5f9f1026f4953979c3a1e60e7ea9f/coverage-7.9.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62f465886fa4f86d5515da525aead97c5dff13a5cf997fc4c5097a1a59e063b2", size = 242918, upload-time = "2025-06-11T23:22:42.88Z" }, + { url = "https://files.pythonhosted.org/packages/6f/eb/273855b57c7fb387dd9787f250b8b333ba8c1c100877c21e32eb1b24ff29/coverage-7.9.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:549ea4ca901595bbe3270e1afdef98bf5d4d5791596efbdc90b00449a2bb1f91", size = 244902, upload-time = "2025-06-11T23:22:44.563Z" }, + { url = "https://files.pythonhosted.org/packages/20/57/4e411b47dbfd831538ecf9e5f407e42888b0c56aedbfe0ea7b102a787559/coverage-7.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8cae1d4450945c74a6a65a09864ed3eaa917055cf70aa65f83ac1b9b0d8d5f9a", size = 245069, upload-time = "2025-06-11T23:22:46.352Z" }, + { url = "https://files.pythonhosted.org/packages/91/75/b24cf5703fb325fc4b1899d89984dac117b99e757b9fadd525cad7ecc020/coverage-7.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d7b263910234c0d5ec913ec79ca921152fe874b805a7bcaf67118ef71708e5d2", size = 243040, upload-time = "2025-06-11T23:22:48.147Z" }, + { url = "https://files.pythonhosted.org/packages/c7/e1/9495751d5315c3d76ee2c7b5dbc1935ab891d45ad585e1910a333dbdef43/coverage-7.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d7b7425215963da8f5968096a20c5b5c9af4a86a950fcc25dcc2177ab33e9e5", size = 244424, upload-time = "2025-06-11T23:22:49.574Z" }, + { url = "https://files.pythonhosted.org/packages/94/2a/ee504188a586da2379939f37fdc69047d9c46d35c34d1196f2605974a17d/coverage-7.9.0-cp313-cp313-win32.whl", hash = "sha256:e7dcfa92867b0c53d2e22e985c66af946dc09e8bb13c556709e396e90a0adf5c", size = 214677, upload-time = "2025-06-11T23:22:51.394Z" }, + { url = "https://files.pythonhosted.org/packages/80/2b/5eab6518643c7560fe180ba5e0f35a0be3d4fc0a88aa6601120407b1fd03/coverage-7.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:aa34ca040785a2b768da489df0c036364d47a6c1c00bdd8f662b98fd3277d3d4", size = 215482, upload-time = "2025-06-11T23:22:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7f/9c9c8b736c4f40d7247bea8339afac40d8f6465491440608b3d73c10ffce/coverage-7.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:9c5dcb5cd3c52d84c5f52045e1c87c16bf189c2fbfa57cc0d811a3b4059939df", size = 213852, upload-time = "2025-06-11T23:22:54.568Z" }, + { url = "https://files.pythonhosted.org/packages/e5/83/056464aec8b360dee6f4d7a517dc5ae5a9f462ff895ff536588b42f95b2d/coverage-7.9.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b52d2fdc1940f90c4572bd48211475a7b102f75a7f9a5e6cfc6e3da7dc380c44", size = 212994, upload-time = "2025-06-11T23:22:56.173Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/f0291ecaa6baaaedbd428cf8b7e1d16b5dc010718fe7739cce955149ef83/coverage-7.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4cc555a3e6ceb8841df01a4634374f5f9635e661f5c307da00bce19819e8bcdf", size = 213212, upload-time = "2025-06-11T23:22:58.051Z" }, + { url = "https://files.pythonhosted.org/packages/16/a0/9eb39541774a5beb662dc4ae98fee23afb947414b6aa1443b53d2ad3ea05/coverage-7.9.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:244f613617876b7cd32a097788d49c952a8f1698afb25275b2a825a4e895854e", size = 256453, upload-time = "2025-06-11T23:22:59.485Z" }, + { url = "https://files.pythonhosted.org/packages/93/33/d0e99f4c809334dfed20f17234080a9003a713ddb80e33ad22697a8aa8e5/coverage-7.9.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c335d77539e66bc6f83e8f1ef207d038129d9b9acd9dc9f0ca42fa9eedf564a", size = 252674, upload-time = "2025-06-11T23:23:00.984Z" }, + { url = "https://files.pythonhosted.org/packages/0b/3a/d2a64e7ee5eb783e44e6ca404f8fc2a45afef052ed6593afb4ce9663dae6/coverage-7.9.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b335c7077c8da7bb8173d4f9ebd90ff1a97af6a6bec4fc4e6db4856ae80b31e", size = 254830, upload-time = "2025-06-11T23:23:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/e2/6a/9de640f8e2b097d155532d1bc16eb9c5186fccc7c4b8148fe1dd2520875a/coverage-7.9.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:01cbc2c36895b7ab906514042c92b3fc9dd0526bf1c3251cb6aefd9c71ae6dda", size = 256060, upload-time = "2025-06-11T23:23:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/07/72/928fa3583b9783fc32e3dfafb6cc0cf73bdd73d1dc41e3a973f203c6aeff/coverage-7.9.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1ac62880a9dff0726a193ce77a1bcdd4e8491009cb3a0510d31381e8b2c46d7a", size = 254174, upload-time = "2025-06-11T23:23:05.366Z" }, + { url = "https://files.pythonhosted.org/packages/ad/01/2fd0785f8768693b748e36b442352bc26edf3391246eedcc80d480d06da1/coverage-7.9.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:95314eb306cf54af3d1147e27ba008cf78eed6f1309a1310772f4f05b12c9c65", size = 255011, upload-time = "2025-06-11T23:23:07.212Z" }, + { url = "https://files.pythonhosted.org/packages/b7/49/1d0120cfa24e001e0d38795388914183c48cd86fc8640ca3b01337831917/coverage-7.9.0-cp313-cp313t-win32.whl", hash = "sha256:c5cbf3ddfb68de8dc8ce33caa9321df27297a032aeaf2e99b278f183fb4ebc37", size = 215349, upload-time = "2025-06-11T23:23:09.037Z" }, + { url = "https://files.pythonhosted.org/packages/9f/48/7625c09621a206fff0b51fcbcf5d6c1162ab10a5ffa546fc132f01c9132b/coverage-7.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e3ec9e1525eb7a0f89d31083539b398d921415d884e9f55400002a1e9fe0cf63", size = 216516, upload-time = "2025-06-11T23:23:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/bb/50/048b55c34985c3aafcecb32cced3abc4291969bfd967dbcaed95cfc26b2a/coverage-7.9.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a02efe6769f74245ce476e89db3d4e110db07b4c0c3d3f81728e2464bbbbcb8e", size = 214308, upload-time = "2025-06-11T23:23:12.522Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4e/4c72909d117d593e388c82b8bc29f99ad0fe20fe84f6390ee14d5650b750/coverage-7.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:64dab59d812c1cbfc9cebadada377365874964acdf59b12e86487d25c2e0c29f", size = 211938, upload-time = "2025-06-11T23:23:14.301Z" }, + { url = "https://files.pythonhosted.org/packages/84/84/8e2e1ebe02a5c68c4ac54668392ee00fa5ea8e7989b339d847fff27220bd/coverage-7.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46b9dc640c6309fb49625d3569d4ba7abe2afcba645eb1e52bad97510f60ac26", size = 212314, upload-time = "2025-06-11T23:23:15.816Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/931117485d6917f4719be2bf8cc25c79c7108c078b005b38882688e1f41b/coverage-7.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89358f4025ed424861311b33815a2866f7c94856c932b0ffc98180f655e813e2", size = 241077, upload-time = "2025-06-11T23:23:17.382Z" }, + { url = "https://files.pythonhosted.org/packages/f9/58/431fbfb00a4dfc1d845b70d296b503d306be76d07a67a4046b15e42c8234/coverage-7.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:589e37ae75d81fd53cd1ca624e07af4466e9e4ce259e3bfe2b147896857c06ea", size = 238945, upload-time = "2025-06-11T23:23:18.936Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e2/8b2cc9b761bee876472379db92d017d7042eeaddba35adf67f54e3ceff3d/coverage-7.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29dea81eef5432076cee561329b3831bc988a4ce1bfaec90eee2078ff5311e6e", size = 240063, upload-time = "2025-06-11T23:23:20.883Z" }, + { url = "https://files.pythonhosted.org/packages/6c/39/e1b0ba8cac5ae66a13475cb08b184f06d89515b6ea6ed45cd678ae2fbcb1/coverage-7.9.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7b3482588772b6b24601d1677aef299af28d6c212c70b0be27bdfc2e10fb00fe", size = 239789, upload-time = "2025-06-11T23:23:22.739Z" }, + { url = "https://files.pythonhosted.org/packages/6e/91/b6b926cd875cd03989abb696ccbbd5895e367e6394dcf7c264180f72d038/coverage-7.9.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2debc0b9481b5fc76f771b3b31e89a0cd8791ad977654940a3523f3f2e5d98fe", size = 238041, upload-time = "2025-06-11T23:23:24.531Z" }, + { url = "https://files.pythonhosted.org/packages/43/ce/de736582c44906b5d6067b650ac851d5f249e246753b9d8f7369e7eea00a/coverage-7.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:304ded640bc2a60f14a2ff0fec98cce4c3f2e573c122f0548728c8dceba5abe7", size = 238977, upload-time = "2025-06-11T23:23:26.168Z" }, + { url = "https://files.pythonhosted.org/packages/27/d3/35317997155b16b140a2c62f09e001a12e244b2d410deb5b8cfa861173f4/coverage-7.9.0-cp39-cp39-win32.whl", hash = "sha256:8e0a3a3f9b968007e1f56418a3586f9a983c84ac4e84d28d1c4f8b76c4226282", size = 214442, upload-time = "2025-06-11T23:23:27.774Z" }, + { url = "https://files.pythonhosted.org/packages/0a/42/d4bcd2900c05bdb5773d47173395c68c147b4ca2564e791c8c9b0ed42c73/coverage-7.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:cb3c07dd71d1ff52156d35ee6fa48458c3cec1add7fcce6a934f977fb80c48a5", size = 215351, upload-time = "2025-06-11T23:23:29.383Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b6/d16966f9439ccc3007e1740960d241420d6ba81502642a4be1da1672a103/coverage-7.9.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:ccf1540a0e82ff525844880f988f6caaa2d037005e57bfe203b71cac7626145d", size = 203927, upload-time = "2025-06-11T23:23:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/70/0d/534c1e35cb7688b5c40de93fcca07e3ddc0287659ff85cd376b1dd3f770f/coverage-7.9.0-py3-none-any.whl", hash = "sha256:79ea9a26b27c963cdf541e1eb9ac05311b012bc367d0e31816f1833b06c81c02", size = 203917, upload-time = "2025-06-11T23:23:32.413Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "45.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/c8/a2a376a8711c1e11708b9c9972e0c3223f5fc682552c82d8db844393d6ce/cryptography-45.0.4.tar.gz", hash = "sha256:7405ade85c83c37682c8fe65554759800a4a8c54b2d96e0f8ad114d31b808d57", size = 744890, upload-time = "2025-06-10T00:03:51.297Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/14/93b69f2af9ba832ad6618a03f8a034a5851dc9a3314336a3d71c252467e1/cryptography-45.0.4-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:680806cf63baa0039b920f4976f5f31b10e772de42f16310a6839d9f21a26b0d", size = 4205335, upload-time = "2025-06-10T00:02:41.64Z" }, + { url = "https://files.pythonhosted.org/packages/67/30/fae1000228634bf0b647fca80403db5ca9e3933b91dd060570689f0bd0f7/cryptography-45.0.4-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4ca0f52170e821bc8da6fc0cc565b7bb8ff8d90d36b5e9fdd68e8a86bdf72036", size = 4431487, upload-time = "2025-06-10T00:02:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5a/7dffcf8cdf0cb3c2430de7404b327e3db64735747d641fc492539978caeb/cryptography-45.0.4-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f3fe7a5ae34d5a414957cc7f457e2b92076e72938423ac64d215722f6cf49a9e", size = 4208922, upload-time = "2025-06-10T00:02:45.334Z" }, + { url = "https://files.pythonhosted.org/packages/c6/f3/528729726eb6c3060fa3637253430547fbaaea95ab0535ea41baa4a6fbd8/cryptography-45.0.4-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:25eb4d4d3e54595dc8adebc6bbd5623588991d86591a78c2548ffb64797341e2", size = 3900433, upload-time = "2025-06-10T00:02:47.359Z" }, + { url = "https://files.pythonhosted.org/packages/d9/4a/67ba2e40f619e04d83c32f7e1d484c1538c0800a17c56a22ff07d092ccc1/cryptography-45.0.4-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ce1678a2ccbe696cf3af15a75bb72ee008d7ff183c9228592ede9db467e64f1b", size = 4464163, upload-time = "2025-06-10T00:02:49.412Z" }, + { url = "https://files.pythonhosted.org/packages/7e/9a/b4d5aa83661483ac372464809c4b49b5022dbfe36b12fe9e323ca8512420/cryptography-45.0.4-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:49fe9155ab32721b9122975e168a6760d8ce4cffe423bcd7ca269ba41b5dfac1", size = 4208687, upload-time = "2025-06-10T00:02:50.976Z" }, + { url = "https://files.pythonhosted.org/packages/db/b7/a84bdcd19d9c02ec5807f2ec2d1456fd8451592c5ee353816c09250e3561/cryptography-45.0.4-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2882338b2a6e0bd337052e8b9007ced85c637da19ef9ecaf437744495c8c2999", size = 4463623, upload-time = "2025-06-10T00:02:52.542Z" }, + { url = "https://files.pythonhosted.org/packages/d8/84/69707d502d4d905021cac3fb59a316344e9f078b1da7fb43ecde5e10840a/cryptography-45.0.4-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:23b9c3ea30c3ed4db59e7b9619272e94891f8a3a5591d0b656a7582631ccf750", size = 4332447, upload-time = "2025-06-10T00:02:54.63Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ee/d4f2ab688e057e90ded24384e34838086a9b09963389a5ba6854b5876598/cryptography-45.0.4-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0a97c927497e3bc36b33987abb99bf17a9a175a19af38a892dc4bbb844d7ee2", size = 4572830, upload-time = "2025-06-10T00:02:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/fe/51/8c584ed426093aac257462ae62d26ad61ef1cbf5b58d8b67e6e13c39960e/cryptography-45.0.4-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6a5bf57554e80f75a7db3d4b1dacaa2764611ae166ab42ea9a72bcdb5d577637", size = 4195746, upload-time = "2025-06-10T00:03:03.94Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/4b0ca4d7af95a704eef2f8f80a8199ed236aaf185d55385ae1d1610c03c2/cryptography-45.0.4-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:46cf7088bf91bdc9b26f9c55636492c1cce3e7aaf8041bbf0243f5e5325cfb2d", size = 4424456, upload-time = "2025-06-10T00:03:05.589Z" }, + { url = "https://files.pythonhosted.org/packages/1d/45/5fabacbc6e76ff056f84d9f60eeac18819badf0cefc1b6612ee03d4ab678/cryptography-45.0.4-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7bedbe4cc930fa4b100fc845ea1ea5788fcd7ae9562e669989c11618ae8d76ee", size = 4198495, upload-time = "2025-06-10T00:03:09.172Z" }, + { url = "https://files.pythonhosted.org/packages/55/b7/ffc9945b290eb0a5d4dab9b7636706e3b5b92f14ee5d9d4449409d010d54/cryptography-45.0.4-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:eaa3e28ea2235b33220b949c5a0d6cf79baa80eab2eb5607ca8ab7525331b9ff", size = 3885540, upload-time = "2025-06-10T00:03:10.835Z" }, + { url = "https://files.pythonhosted.org/packages/7f/e3/57b010282346980475e77d414080acdcb3dab9a0be63071efc2041a2c6bd/cryptography-45.0.4-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7ef2dde4fa9408475038fc9aadfc1fb2676b174e68356359632e980c661ec8f6", size = 4452052, upload-time = "2025-06-10T00:03:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/37/e6/ddc4ac2558bf2ef517a358df26f45bc774a99bf4653e7ee34b5e749c03e3/cryptography-45.0.4-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6a3511ae33f09094185d111160fd192c67aa0a2a8d19b54d36e4c78f651dc5ad", size = 4198024, upload-time = "2025-06-10T00:03:13.976Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c0/85fa358ddb063ec588aed4a6ea1df57dc3e3bc1712d87c8fa162d02a65fc/cryptography-45.0.4-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:06509dc70dd71fa56eaa138336244e2fbaf2ac164fc9b5e66828fccfd2b680d6", size = 4451442, upload-time = "2025-06-10T00:03:16.248Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/362d6ec1492596e73da24e669a7fbbaeb1c428d6bf49a29f7a12acffd5dc/cryptography-45.0.4-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5f31e6b0a5a253f6aa49be67279be4a7e5a4ef259a9f33c69f7d1b1191939872", size = 4325038, upload-time = "2025-06-10T00:03:18.4Z" }, + { url = "https://files.pythonhosted.org/packages/53/75/82a14bf047a96a1b13ebb47fb9811c4f73096cfa2e2b17c86879687f9027/cryptography-45.0.4-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:944e9ccf67a9594137f942d5b52c8d238b1b4e46c7a0c2891b7ae6e01e7c80a4", size = 4560964, upload-time = "2025-06-10T00:03:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b9/357f18064ec09d4807800d05a48f92f3b369056a12f995ff79549fbb31f1/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7aad98a25ed8ac917fdd8a9c1e706e5a0956e06c498be1f713b61734333a4507", size = 4143732, upload-time = "2025-06-10T00:03:27.896Z" }, + { url = "https://files.pythonhosted.org/packages/c4/9c/7f7263b03d5db329093617648b9bd55c953de0b245e64e866e560f9aac07/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3530382a43a0e524bc931f187fc69ef4c42828cf7d7f592f7f249f602b5a4ab0", size = 4385424, upload-time = "2025-06-10T00:03:29.992Z" }, + { url = "https://files.pythonhosted.org/packages/a6/5a/6aa9d8d5073d5acc0e04e95b2860ef2684b2bd2899d8795fc443013e263b/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:6b613164cb8425e2f8db5849ffb84892e523bf6d26deb8f9bb76ae86181fa12b", size = 4142438, upload-time = "2025-06-10T00:03:31.782Z" }, + { url = "https://files.pythonhosted.org/packages/42/1c/71c638420f2cdd96d9c2b287fec515faf48679b33a2b583d0f1eda3a3375/cryptography-45.0.4-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:96d4819e25bf3b685199b304a0029ce4a3caf98947ce8a066c9137cc78ad2c58", size = 4384622, upload-time = "2025-06-10T00:03:33.491Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a7d5bb87d149eb99a5abdc69a41e4e47b8001d767e5f403f78bfaafc7aa7/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:03dbff8411206713185b8cebe31bc5c0eb544799a50c09035733716b386e61a4", size = 4146899, upload-time = "2025-06-10T00:03:38.659Z" }, + { url = "https://files.pythonhosted.org/packages/17/11/9361c2c71c42cc5c465cf294c8030e72fb0c87752bacbd7a3675245e3db3/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51dfbd4d26172d31150d84c19bbe06c68ea4b7f11bbc7b3a5e146b367c311349", size = 4388900, upload-time = "2025-06-10T00:03:40.233Z" }, + { url = "https://files.pythonhosted.org/packages/c0/76/f95b83359012ee0e670da3e41c164a0c256aeedd81886f878911581d852f/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:0339a692de47084969500ee455e42c58e449461e0ec845a34a6a9b9bf7df7fb8", size = 4146422, upload-time = "2025-06-10T00:03:41.827Z" }, + { url = "https://files.pythonhosted.org/packages/09/ad/5429fcc4def93e577a5407988f89cf15305e64920203d4ac14601a9dc876/cryptography-45.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:0cf13c77d710131d33e63626bd55ae7c0efb701ebdc2b3a7952b9b23a0412862", size = 4388475, upload-time = "2025-06-10T00:03:43.493Z" }, +] + +[[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 = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[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 = "execnet" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/ff/b4c0dc78fbe20c3e59c0c7334de0c27eb4001a2b2017999af398bf730817/execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3", size = 166524, upload-time = "2024-04-08T09:04:19.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/09/2aea36ff60d16dd8879bdb2f5b3ee0ba8d08cbbdcdfe870e695ce3784385/execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc", size = 40612, upload-time = "2024-04-08T09:04:17.414Z" }, +] + +[[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 = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + +[[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" }, +] +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 = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159, upload-time = "2024-09-27T19:47:09.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187, upload-time = "2024-09-27T19:47:07.14Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[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 = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "markdown" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "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 = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "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-click" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "markdown" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/c7/8c25f3a3b379def41e6d0bb5c4beeab7aa8a394b17e749f498504102cfa5/mkdocs_click-0.9.0.tar.gz", hash = "sha256:6050917628d4740517541422b607404d044117bc31b770c4f9e9e1939a50c908", size = 18720, upload-time = "2025-04-07T16:59:36.387Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/fc/9124ab36e2341e78d8d9c669511bd70f52ea0de8105760c31fabec1f9396/mkdocs_click-0.9.0-py3-none-any.whl", hash = "sha256:5208e828f4f68f63c847c1ef7be48edee9964090390afc8f5b3d4cbe5ea9bbed", size = 15104, upload-time = "2025-04-07T16:59:34.807Z" }, +] + +[[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 = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + +[[package]] +name = "mypy" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, + { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, + { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, + { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, + { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, + { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, + { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/bd/eb/c0759617fe2159aee7a653f13cceafbf7f0b6323b4197403f2e587ca947d/mypy-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f56236114c425620875c7cf71700e3d60004858da856c6fc78998ffe767b73d3", size = 10956081, upload-time = "2025-05-29T13:19:32.264Z" }, + { url = "https://files.pythonhosted.org/packages/70/35/df3c74a2967bdf86edea58b265feeec181d693432faed1c3b688b7c231e3/mypy-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:15486beea80be24ff067d7d0ede673b001d0d684d0095803b3e6e17a886a2a92", size = 10084422, upload-time = "2025-05-29T13:18:01.437Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/145ffe29f4b577219943b7b1dc0a71df7ead3c5bed4898686bd87c5b5cc2/mypy-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2ed0e0847a80655afa2c121835b848ed101cc7b8d8d6ecc5205aedc732b1436", size = 11879670, upload-time = "2025-05-29T13:17:45.971Z" }, + { url = "https://files.pythonhosted.org/packages/c6/94/0421562d6b046e22986758c9ae31865d10ea0ba607ae99b32c9d18b16f66/mypy-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb5fbc8063cb4fde7787e4c0406aa63094a34a2daf4673f359a1fb64050e9cb2", size = 12610528, upload-time = "2025-05-29T13:34:36.983Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f1/39a22985b78c766a594ae1e0bbb6f8bdf5f31ea8d0c52291a3c211fd3cd5/mypy-1.16.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a5fcfdb7318c6a8dd127b14b1052743b83e97a970f0edb6c913211507a255e20", size = 12871923, upload-time = "2025-05-29T13:32:21.823Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8e/84db4fb0d01f43d2c82fa9072ca72a42c49e52d58f44307bbd747c977bc2/mypy-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:2e7e0ad35275e02797323a5aa1be0b14a4d03ffdb2e5f2b0489fa07b89c67b21", size = 9482931, upload-time = "2025-05-29T13:21:32.326Z" }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nh3" +version = "0.2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581, upload-time = "2025-02-25T13:38:44.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678, upload-time = "2025-02-25T13:37:56.063Z" }, + { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774, upload-time = "2025-02-25T13:37:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012, upload-time = "2025-02-25T13:38:01.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619, upload-time = "2025-02-25T13:38:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384, upload-time = "2025-02-25T13:38:04.402Z" }, + { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908, upload-time = "2025-02-25T13:38:06.693Z" }, + { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180, upload-time = "2025-02-25T13:38:10.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747, upload-time = "2025-02-25T13:38:12.548Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908, upload-time = "2025-02-25T13:38:14.059Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133, upload-time = "2025-02-25T13:38:16.601Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328, upload-time = "2025-02-25T13:38:18.972Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020, upload-time = "2025-02-25T13:38:20.571Z" }, + { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878, upload-time = "2025-02-25T13:38:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460, upload-time = "2025-02-25T13:38:25.951Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369, upload-time = "2025-02-25T13:38:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036, upload-time = "2025-02-25T13:38:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712, upload-time = "2025-02-25T13:38:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559, upload-time = "2025-02-25T13:38:35.204Z" }, + { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591, upload-time = "2025-02-25T13:38:37.099Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670, upload-time = "2025-02-25T13:38:38.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093, upload-time = "2025-02-25T13:38:40.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623, upload-time = "2025-02-25T13:38:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283, upload-time = "2025-02-25T13:38:43.355Z" }, +] + +[[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 = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[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 = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.0" +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/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, +] + +[[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 = "pytest-mock" +version = "3.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973, upload-time = "2025-05-05T19:44:34.99Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/dc/865845cfe987b21658e871d16e0a24e871e00884c545f246dd8f6f69edda/pytest_xdist-3.7.0.tar.gz", hash = "sha256:f9248c99a7c15b7d2f90715df93610353a485827bc06eefb6566d23f6400f126", size = 87550, upload-time = "2025-05-26T21:18:20.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/b2/0e802fde6f1c5b2f7ae7e9ad42b83fd4ecebac18a8a8c2f2f14e39dce6e1/pytest_xdist-3.7.0-py3-none-any.whl", hash = "sha256:7d3fbd255998265052435eb9daa4e99b62e6fb9cfb6efd1f858d4d8c0c7f0ca0", size = 46142, upload-time = "2025-05-26T21:18:18.759Z" }, +] + +[[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 = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[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 = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[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 = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, +] + +[[package]] +name = "rich-click" +version = "1.8.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/a8/dcc0a8ec9e91d76ecad9413a84b6d3a3310c6111cfe012d75ed385c78d96/rich_click-1.8.9.tar.gz", hash = "sha256:fd98c0ab9ddc1cf9c0b7463f68daf28b4d0033a74214ceb02f761b3ff2af3136", size = 39378, upload-time = "2025-05-19T21:33:05.569Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/c2/9fce4c8a9587c4e90500114d742fe8ef0fd92d7bad29d136bb9941add271/rich_click-1.8.9-py3-none-any.whl", hash = "sha256:c3fa81ed8a671a10de65a9e20abf642cfdac6fdb882db1ef465ee33919fbcfe2", size = 36082, upload-time = "2025-05-19T21:33:04.195Z" }, +] + +[[package]] +name = "ruff" +version = "0.11.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[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 = "toady-cli" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "click", version = "8.2.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "rich" }, + { name = "rich-click" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "black" }, + { name = "build" }, + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "psutil" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-timeout" }, + { name = "pytest-xdist" }, + { name = "python-dotenv" }, + { name = "ruff" }, + { name = "twine" }, + { name = "types-click" }, + { name = "types-psutil" }, +] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-click" }, + { name = "mkdocs-material" }, +] +test = [ + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "pytest-mock" }, + { name = "pytest-xdist" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "black", marker = "extra == 'dev'", specifier = ">=24.8.0" }, + { name = "build", marker = "extra == 'dev'", specifier = ">=1.2.0" }, + { name = "click", specifier = ">=8.1.7" }, + { name = "mkdocs", marker = "extra == 'dev'", specifier = ">=1.6.0" }, + { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6.0" }, + { name = "mkdocs-click", marker = "extra == 'docs'", specifier = ">=0.8.1" }, + { name = "mkdocs-material", marker = "extra == 'dev'", specifier = ">=9.5.0" }, + { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.13.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.0" }, + { name = "psutil", marker = "extra == 'dev'", specifier = ">=6.1.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" }, + { name = "pytest", marker = "extra == 'test'", specifier = ">=8.3.0" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, + { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=6.0.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, + { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.14.0" }, + { name = "pytest-timeout", marker = "extra == 'dev'", specifier = ">=2.3.1" }, + { name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.6.0" }, + { name = "pytest-xdist", marker = "extra == 'test'", specifier = ">=3.6.0" }, + { name = "python-dotenv", marker = "extra == 'dev'", specifier = ">=1.0.1" }, + { name = "python-dotenv", marker = "extra == 'test'", specifier = ">=1.0.1" }, + { name = "rich", specifier = ">=13.9.0" }, + { name = "rich-click", specifier = ">=1.8.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.8.0" }, + { name = "twine", marker = "extra == 'dev'", specifier = ">=6.0.0" }, + { name = "types-click", marker = "extra == 'dev'", specifier = ">=7.1.8" }, + { name = "types-psutil", marker = "extra == 'dev'", specifier = ">=6.1.0" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'", specifier = ">=4.12.0" }, +] +provides-extras = ["dev", "test", "docs"] + +[[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 = "twine" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10'" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/a2/6df94fc5c8e2170d21d7134a565c3a8fb84f9797c1dd65a5976aaf714418/twine-6.1.0.tar.gz", hash = "sha256:be324f6272eff91d07ee93f251edf232fc647935dd585ac003539b42404a8dbd", size = 168404, upload-time = "2025-01-21T18:45:26.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/b6/74e927715a285743351233f33ea3c684528a0d374d2e43ff9ce9585b73fe/twine-6.1.0-py3-none-any.whl", hash = "sha256:a47f973caf122930bf0fbbf17f80b83bc1602c9ce393c7845f289a3001dc5384", size = 40791, upload-time = "2025-01-21T18:45:24.584Z" }, +] + +[[package]] +name = "types-click" +version = "7.1.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/ff/0e6a56108d45c80c61cdd4743312d0304d8192482aea4cce96c554aaa90d/types-click-7.1.8.tar.gz", hash = "sha256:b6604968be6401dc516311ca50708a0a28baa7a0cb840efd7412f0dbbff4e092", size = 10015, upload-time = "2021-11-23T12:28:01.701Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/ad/607454a5f991c5b3e14693a7113926758f889138371058a5f72f567fa131/types_click-7.1.8-py3-none-any.whl", hash = "sha256:8cb030a669e2e927461be9827375f83c16b8178c365852c060a34e24871e7e81", size = 12929, upload-time = "2021-11-23T12:27:59.493Z" }, +] + +[[package]] +name = "types-psutil" +version = "7.0.0.20250601" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c8/af/767b92be7de4105f5e2e87a53aac817164527c4a802119ad5b4e23028f7c/types_psutil-7.0.0.20250601.tar.gz", hash = "sha256:71fe9c4477a7e3d4f1233862f0877af87bff057ff398f04f4e5c0ca60aded197", size = 20297, upload-time = "2025-06-01T03:25:16.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/85/864c663a924a34e0d87bd10ead4134bb4ab6269fa02daaa5dd644ac478c5/types_psutil-7.0.0.20250601-py3-none-any.whl", hash = "sha256:0c372e2d1b6529938a080a6ba4a9358e3dfc8526d82fabf40c1ef9325e4ca52e", size = 23106, upload-time = "2025-06-01T03:25:15.386Z" }, +] + +[[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.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] + +[[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" }, +]