From 8aafcf4791f6c85cf054ac2abe53d64b0c85aaf0 Mon Sep 17 00:00:00 2001 From: falamarcao Date: Wed, 6 Aug 2025 01:09:58 -0300 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=90=9B=20fix(server):=20suppress=20Fa?= =?UTF-8?q?stAPI=20proxy=20=E2=80=9CDuplicate=20Operation=20ID=E2=80=9D=20?= =?UTF-8?q?warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/routers/selenium_proxy.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/routers/selenium_proxy.py b/src/app/routers/selenium_proxy.py index 133d57c..6482da2 100644 --- a/src/app/routers/selenium_proxy.py +++ b/src/app/routers/selenium_proxy.py @@ -174,12 +174,7 @@ async def selenium_hub_ui_redirect() -> RedirectResponse: return RedirectResponse(url=f"{SELENIUM_HUB_PREFIX}/ui/") -@router.api_route( - "/{path:path}", - methods=["GET", "POST", "DELETE"], - response_class=Response, -) -async def selenium_hub_path_proxy( +async def selenium_hub_proxy( request: Request, path: str, settings: Annotated[Settings, Depends(get_settings)], @@ -190,6 +185,17 @@ async def selenium_hub_path_proxy( return await proxy_selenium_request(request, selenium_url, basic_auth) +# Define routes for selenium_hub_path_proxy - `Avoiding UserWarning: Duplicate Operation ID` +for method in ["GET", "POST", "DELETE"]: + router.api_route( + "/{path:path}", + methods=[method], + name=f"selenium_hub_{method.lower()}", + response_class=Response, + )(selenium_hub_proxy) + + +# Selenium grid old versions # @router.api_route( # "/ui/{path:path}", # methods=["GET", "POST", "DELETE"], From e838a144cf87e4bdcfc723c684d1eb54ffe0c23f Mon Sep 17 00:00:00 2001 From: falamarcao Date: Wed, 6 Aug 2025 02:37:00 -0300 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8E=89=20refactor(cli):=20modular=20M?= =?UTF-8?q?CP=20Selenium=20Grid=20CLI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit β€’ πŸ—οΈ Create cli module and restructure application β€’ πŸš€ Move helm cli inside mcp selenium grid cli β€’ πŸ—‘οΈ Remove custom pydantic models, use default Typer validation β€’ πŸ“š Update README documentation β€’ 🐳 Update Dockerfile for new structure --- .github/workflows/1_tests.yml | 3 +- CONTRIBUTING.md | 4 +- Dockerfile | 1 - README.md | 39 ++++---- config.yaml | 2 +- deployment/README.md | 2 + pyproject.toml | 11 ++- scripts/helm/README.md | 94 ------------------ scripts/helm/cli/__init__.py | 0 scripts/helm/models.py | 51 ---------- src/app/__main__.py | 77 +-------------- src/app/cli/__init__.py | 3 + {scripts => src/app/cli}/helm/__init__.py | 2 +- {scripts => src/app/cli/helm/cli}/__init__.py | 0 {scripts => src/app/cli}/helm/cli/helm.py | 0 {scripts => src/app/cli}/helm/cli/helpers.py | 0 {scripts => src/app/cli}/helm/cli/kubectl.py | 0 {scripts => src/app/cli}/helm/cli/py.typed | 0 {scripts => src/app/cli}/helm/helpers.py | 19 +--- {scripts => src/app/cli}/helm/main.py | 97 ++++++++----------- src/app/cli/main.py | 77 +++++++++++++++ src/app/cli/utils.py | 22 +++++ uv.lock | 2 +- 23 files changed, 182 insertions(+), 324 deletions(-) delete mode 100644 scripts/helm/README.md delete mode 100644 scripts/helm/cli/__init__.py delete mode 100644 scripts/helm/models.py create mode 100644 src/app/cli/__init__.py rename {scripts => src/app/cli}/helm/__init__.py (65%) rename {scripts => src/app/cli/helm/cli}/__init__.py (100%) rename {scripts => src/app/cli}/helm/cli/helm.py (100%) rename {scripts => src/app/cli}/helm/cli/helpers.py (100%) rename {scripts => src/app/cli}/helm/cli/kubectl.py (100%) rename {scripts => src/app/cli}/helm/cli/py.typed (100%) rename {scripts => src/app/cli}/helm/helpers.py (83%) rename {scripts => src/app/cli}/helm/main.py (65%) create mode 100644 src/app/cli/main.py create mode 100644 src/app/cli/utils.py diff --git a/.github/workflows/1_tests.yml b/.github/workflows/1_tests.yml index d066dfb..30a0494 100644 --- a/.github/workflows/1_tests.yml +++ b/.github/workflows/1_tests.yml @@ -30,12 +30,11 @@ jobs: filters: | py: - 'src/**/*.py' - - 'scripts/**/*.py' lint: uses: ./.github/workflows/1.1_lint.yml needs: filter - if: github.ref == 'refs/heads/main' && needs.filter.outputs.src_changed == 'true' + if: needs.filter.outputs.src_changed == 'true' permissions: contents: read unit-tests: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6fd7f33..23db6e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -131,6 +131,8 @@ kubectl config set-context k3s-selenium-grid \ ```bash # See command help uv run mcp-selenium-grid helm --help +uv run mcp-selenium-grid helm deploy --help +uv run mcp-selenium-grid helm uninstall --help # Deploy using default config uv run mcp-selenium-grid helm deploy @@ -142,8 +144,6 @@ uv run mcp-selenium-grid helm deploy --context k3s-selenium-grid uv run mcp-selenium-grid helm uninstall --delete-namespace ``` -> See [scripts/helm/README.md](scripts/helm/README.md) for more details. - ### 4. Start Server ```bash diff --git a/Dockerfile b/Dockerfile index 71ed14d..829950c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,6 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv # Copy project files COPY pyproject.toml config.yaml LICENSE README.md uv.lock ./mcp-selenium-grid/ COPY deployment ./mcp-selenium-grid/deployment -COPY scripts ./mcp-selenium-grid/scripts COPY src/app ./mcp-selenium-grid/src/app # Install the application dependencies diff --git a/README.md b/README.md index 3e30c77..6988332 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,18 @@ For Docker-based deployment, ensure Docker is running and use the Docker configu "command": "uvx", "args": ["mcp-selenium-grid", "server", "run", "--host", "127.0.0.1", - "--port", "8000", + "--port", "8000" ], "env": { "API_TOKEN": "CHANGE_ME", - "ALLOWED_ORIGINS": ["http://localhost:8000"], + "ALLOWED_ORIGINS": "[\"http://127.0.0.1:8000\"]", "DEPLOYMENT_MODE": "docker", "SELENIUM_GRID__USERNAME": "USER", "SELENIUM_GRID__PASSWORD": "CHANGE_ME", "SELENIUM_GRID__VNC_PASSWORD": "CHANGE_ME", - "SELENIUM_GRID__VNC_VIEW_ONLY": false, - "SELENIUM_GRID__MAX_BROWSER_INSTANCES": 4, - "SELENIUM_GRID__SE_NODE_MAX_SESSIONS": 1, + "SELENIUM_GRID__VNC_VIEW_ONLY": "false", + "SELENIUM_GRID__MAX_BROWSER_INSTANCES": "4", + "SELENIUM_GRID__SE_NODE_MAX_SESSIONS": "1" } } } @@ -104,24 +104,25 @@ kubectl config set-context k3s-selenium-grid \ ###### Deploy Selenium Grid -Using kubernetes context name from [config.yaml](./config.yaml): +Please run for help to get to know the available commands and parameters: ```bash uvx mcp-selenium-grid helm --help -uvx mcp-selenium-grid helm deploy +uvx mcp-selenium-grid helm deploy --help +uvx mcp-selenium-grid helm uninstall --help ``` -For a given kubernetes context name: +Deploy using default parameters: ```bash -uvx mcp-selenium-grid helm deploy --context k3s-selenium-grid +uvx mcp-selenium-grid helm deploy ``` -Uninstall: +Uninstall using default parameters: ```bash +# using default parameters uvx mcp-selenium-grid helm uninstall --delete-namespace -uvx mcp-selenium-grid helm uninstall --context k3s-selenium-grid --delete-namespace ``` ```json @@ -131,22 +132,22 @@ uvx mcp-selenium-grid helm uninstall --context k3s-selenium-grid --delete-namesp "command": "uvx", "args": ["mcp-selenium-grid", "server", "run", "--host", "127.0.0.1", - "--port", "8000", + "--port", "8000" ], "env": { "API_TOKEN": "CHANGE_ME", - "ALLOWED_ORIGINS": ["http://localhost:8000"], + "ALLOWED_ORIGINS": "[\"http://127.0.0.1:8000\"]", "DEPLOYMENT_MODE": "kubernetes", "SELENIUM_GRID__USERNAME": "USER", "SELENIUM_GRID__PASSWORD": "CHANGE_ME", "SELENIUM_GRID__VNC_PASSWORD": "CHANGE_ME", - "SELENIUM_GRID__VNC_VIEW_ONLY": false, - "SELENIUM_GRID__MAX_BROWSER_INSTANCES": 4, - "SELENIUM_GRID__SE_NODE_MAX_SESSIONS": 1, + "SELENIUM_GRID__VNC_VIEW_ONLY": "false", + "SELENIUM_GRID__MAX_BROWSER_INSTANCES": "4", + "SELENIUM_GRID__SE_NODE_MAX_SESSIONS": "1", "KUBERNETES__KUBECONFIG": "~/.kube/config-local-k3s", "KUBERNETES__CONTEXT": "k3s-selenium-grid", "KUBERNETES__NAMESPACE": "selenium-grid-dev", - "KUBERNETES__SELENIUM_GRID_SERVICE_NAME": "selenium-grid", + "KUBERNETES__SELENIUM_GRID_SERVICE_NAME": "selenium-grid" } } } @@ -165,7 +166,7 @@ uvx mcp-selenium-grid helm uninstall --context k3s-selenium-grid --delete-namesp "--init", "-p", "8000:80", "-e", "API_TOKEN=CHANGE_ME", - "-e", "ALLOWED_ORIGINS=http://localhost:8000", + "-e", "ALLOWED_ORIGINS=[\"http://127.0.0.1:8000\"]", "-e", "DEPLOYMENT_MODE=kubernetes", // required for docker "-e", "SELENIUM_GRID__USERNAME=USER", "-e", "SELENIUM_GRID__PASSWORD=CHANGE_ME", @@ -177,7 +178,7 @@ uvx mcp-selenium-grid helm uninstall --context k3s-selenium-grid --delete-namesp "-e", "KUBERNETES__CONTEXT=k3s-selenium-grid", "-e", "KUBERNETES__NAMESPACE=selenium-grid-dev", "-e", "KUBERNETES__SELENIUM_GRID_SERVICE_NAME=selenium-grid", - "ghcr.io/falamarcao/mcp-selenium-grid:latest", + "ghcr.io/falamarcao/mcp-selenium-grid:latest" ] } } diff --git a/config.yaml b/config.yaml index 5ead9b4..2e4e6a1 100644 --- a/config.yaml +++ b/config.yaml @@ -23,7 +23,7 @@ selenium_grid: memory: "1G" cpu: "0.5" undetected-chrome: - image: falamarcao/node-undetected-chrome:latest # not public yet (testing) + image: ghcr.io/catchnip/node-undetected-chrome:latest # not public yet (testing) port: 4444 resources: memory: "1G" diff --git a/deployment/README.md b/deployment/README.md index 8436a6a..4a077cc 100644 --- a/deployment/README.md +++ b/deployment/README.md @@ -2,6 +2,8 @@ This directory contains the deployment configurations for the Selenium Grid on Kubernetes using Helm. +⚠️ Not meant to be used directly. Use `uvx mcp-selenium-grid helm deploy --help` instead. ⚠️ + ## Directory Structure ```txt diff --git a/pyproject.toml b/pyproject.toml index 7c102ec..f881d04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,14 +3,17 @@ # AI Agents and contributors: Please assume uv is the default tool for all dependency and environment management in this project. # You do NOT need to ask the user if uv is being usedβ€”always use uv commands for installs, updates, and checks. # -# To install dependencies: `uv pip install .` or `uv pip install -e ".[dev]"` (for development) -# To add a dependency: `uv pip install ` +# - Install all dependencies: `uv sync --all-groups --extra test --extra coverage` +# - Add a dependency: `uv add ` +# - Add a dev dependency: `uv add --dev` +# - Add a test dependency: `uv add --optional test` +# - Remove a dependency: `uv remove ` # # For more info, see the README. [project] name = "mcp-selenium-grid" -version = "0.1.0.dev1" +version = "0.1.0.dev2" description = "MCP Server for managing Selenium Grid instances" readme = "README.md" license = { file = "LICENSE" } @@ -65,7 +68,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] -packages = ["src/app", "scripts"] +packages = ["src/app"] [tool.ruff] line-length = 100 diff --git a/scripts/helm/README.md b/scripts/helm/README.md deleted file mode 100644 index a60d8d8..0000000 --- a/scripts/helm/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Helm Selenium Grid CLI - -This command-line interface (CLI) tool, `helm-selenium-grid`, is designed to simplify the deployment and management of the Selenium Grid on Kubernetes using Helm. - -It interacts with your `config.yaml` for default settings but allows overriding these settings via command-line options. - -## Installation - -This script is part of the MCP Selenium Grid project. Ensure you have followed the main project's Quick Start for Development to set up your environment and install dependencies. - -The script is made available as `helm-selenium-grid` via the `pyproject.toml` configuration: - -```toml -[project.scripts] -helm-selenium-grid = "scripts.helm.main:app" -``` - -You can run the commands using `uv run helm-selenium-grid [OPTIONS]`. - -## Commands - -### `deploy` - -Deploys or upgrades the Selenium Grid Helm release on your Kubernetes cluster. - -**Usage:** - -```bash -uv run helm-selenium-grid deploy [OPTIONS] -``` - -**Options:** - -- `--chart-path PATH`: Path to the Helm chart. - - Default: `deployment/helm/selenium-grid` -- `--release-name TEXT`: Name of the Helm release. - - Default: `selenium-grid` -- `--namespace TEXT`: Kubernetes namespace. - - Default: Value from `config.yaml` (`NAMESPACE`) -- `--context TEXT`: Kubernetes context to use (e.g., 'k3s'). Overrides context from `config.yaml`. - - Default: Value from `config.yaml` (`CONTEXT`) -- `--kubeconfig PATH`: Path to the kubeconfig file. Overrides `KUBECONFIG` from settings. - - Default: None -- `--debug`: Enable debug output. - - Default: `False` -- `--help`: Show help message and exit. - -**Example:** - -```bash -# Deploy using defaults from config.yaml -uv run helm-selenium-grid deploy - -# Deploy to a specific Kubernetes context and namespace -uv run helm-selenium-grid deploy --kube-context k3s --namespace selenium -``` - -### `uninstall` - -Uninstalls the Selenium Grid Helm release from your Kubernetes cluster. - -**Usage:** - -```bash -uv run helm-selenium-grid uninstall [OPTIONS] -``` - -**Options:** - -- `--release-name TEXT`: Name of the Helm release to uninstall. - - Default: `selenium-grid` -- `--namespace TEXT`: Kubernetes namespace. - - Default: Value from `config.yaml` (`NAMESPACE`) -- `--context TEXT`: Kubernetes context to use. Overrides context from `config.yaml`. - - Default: Value from `config.yaml` (`CONTEXT`) -- `--kubeconfig PATH`: Path to the kubeconfig file. Overrides `KUBECONFIG` from settings. - - Default: None -- `--delete-namespace`: Delete the Kubernetes namespace after uninstalling the release. - - Default: `False` -- `--debug`: Enable debug output. - - Default: `False` -- `--help`: Show help message and exit. - -**Example:** - -```bash -# Uninstall using defaults from config.yaml -uv run helm-selenium-grid uninstall - -# Uninstall and delete namespace from a specific Kubernetes context and namespace -uv run helm-selenium-grid uninstall --context k3s-selenium-grid --namespace selenium-grid-dev --delete-namespace -``` - -For more detailed deployment information, including configuration options and troubleshooting, see [Deployment Guide](src/deployment/README.md). diff --git a/scripts/helm/cli/__init__.py b/scripts/helm/cli/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/scripts/helm/models.py b/scripts/helm/models.py deleted file mode 100644 index e134006..0000000 --- a/scripts/helm/models.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Models for Helm Selenium Grid deployment.""" - -import re -from pathlib import Path - -from pydantic import BaseModel, Field, field_validator - - -class K8sName(BaseModel): - """Kubernetes resource name with validation.""" - - name: str = Field(..., description="Kubernetes resource name") - - @field_validator("name") - @classmethod - def validate_name(cls, v: str) -> str: - """Validate Kubernetes resource name format.""" - K8S_NAME_PATTERN = r"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" - MAX_NAME_LENGTH = 63 - - if not re.match(K8S_NAME_PATTERN, v): - raise ValueError(f"Invalid name: {v}") - if len(v) > MAX_NAME_LENGTH: - raise ValueError(f"Name too long (max {MAX_NAME_LENGTH} characters)") - return v - - def __str__(self) -> str: - return self.name - - -class HelmChartPath(BaseModel): - """Helm chart path with validation.""" - - path: Path = Field(..., description="Path to the Helm chart") - - @field_validator("path") - @classmethod - def validate_path(cls, v: Path) -> Path: - """Validate Helm chart path exists and is within project directory.""" - if not v.is_absolute(): - v = v.resolve() - - project_root = Path(__file__).parent.parent.parent - try: - v.relative_to(project_root) - except ValueError: - raise ValueError(f"Helm chart path {v} is outside project directory") - - if not v.exists(): - raise FileNotFoundError(f"Helm chart directory not found at {v}") - return v diff --git a/src/app/__main__.py b/src/app/__main__.py index 726dad8..b5a7d2f 100644 --- a/src/app/__main__.py +++ b/src/app/__main__.py @@ -1,83 +1,10 @@ """Entry point for running the MCP Selenium Grid server.""" -from fastapi_cli.cli import dev, run -from scripts.helm.main import create_application as create_helm_app -from typer import Typer - - -def create_application() -> Typer: - __doc__title__ = """ - [bold green_yellow]MCP Selenium Grid Server[/bold green_yellow] πŸš€ - Run the [turquoise4]MCP Server/REST API[/turquoise4] or manage [turquoise4]Kubernetes[/turquoise4] deployments. - """ - __doc__ = ( - __doc__title__ - + """ - - [pale_turquoise1]Model Context Protocol (MCP) server that enables AI Agents to request - and manage Selenium browser instances through a secure API.[/pale_turquoise1] - - [italic gold1]Perfect for your automated browser testing needs![/italic gold1] - - Read more in the docs: - [link=https://github.com/Falamarcao/mcp-selenium-grid]https://github.com/Falamarcao/mcp-selenium-grid[/link] - """ - ) - - app = Typer( - name="mcp-selenium-grid", - help=__doc__, - rich_help_panel="main", - rich_markup_mode="rich", - add_completion=False, - no_args_is_help=True, # optional quality of life - pretty_exceptions_show_locals=False, - ) - - custom_fastapi_cli = Typer(help="Custom FastAPI CLI with limited commands.") - - # Add specific commands - custom_fastapi_cli.command( - name="dev", - help="Run a Selenium Grid MCP Server in [bright_green]development[/bright_green] mode", - )(dev) - custom_fastapi_cli.command( - name="run", - help="Run a Selenium Grid MCP Server in [bright_green]production[/bright_green] mode", - )(run) - - app.add_typer( - custom_fastapi_cli, - name="server", - help=__doc__, - ) - - # Import and add the Helm subcommand - try: - __doc__helm = ( - __doc__title__ - + """ - - [pale_turquoise1]Manage Kubernetes deployments with Helm ☸️[/pale_turquoise1] - [italic gold1]Prepare the kubernetes cluster to run MCP Selenium Grid![/italic gold1] - """ - ) - # Create the Helm app and add it as a subcommand group - helm_app = create_helm_app() - app.add_typer(helm_app, name="helm", help=__doc__helm) - - except ImportError: - # If Helm dependencies are not available, skip adding the commands - pass - - return app - - -app = create_application() +from .cli import mcp_selenium_grid_cli def main() -> None: - app() + mcp_selenium_grid_cli() if __name__ == "__main__": diff --git a/src/app/cli/__init__.py b/src/app/cli/__init__.py new file mode 100644 index 0000000..4f43560 --- /dev/null +++ b/src/app/cli/__init__.py @@ -0,0 +1,3 @@ +from .main import app as mcp_selenium_grid_cli + +__all__ = ["mcp_selenium_grid_cli"] diff --git a/scripts/helm/__init__.py b/src/app/cli/helm/__init__.py similarity index 65% rename from scripts/helm/__init__.py rename to src/app/cli/helm/__init__.py index 00aca9e..d48f13f 100644 --- a/scripts/helm/__init__.py +++ b/src/app/cli/helm/__init__.py @@ -1,5 +1,5 @@ """Helm Selenium Grid deployment package.""" -from scripts.helm.main import app +from .main import app __all__ = ["app"] diff --git a/scripts/__init__.py b/src/app/cli/helm/cli/__init__.py similarity index 100% rename from scripts/__init__.py rename to src/app/cli/helm/cli/__init__.py diff --git a/scripts/helm/cli/helm.py b/src/app/cli/helm/cli/helm.py similarity index 100% rename from scripts/helm/cli/helm.py rename to src/app/cli/helm/cli/helm.py diff --git a/scripts/helm/cli/helpers.py b/src/app/cli/helm/cli/helpers.py similarity index 100% rename from scripts/helm/cli/helpers.py rename to src/app/cli/helm/cli/helpers.py diff --git a/scripts/helm/cli/kubectl.py b/src/app/cli/helm/cli/kubectl.py similarity index 100% rename from scripts/helm/cli/kubectl.py rename to src/app/cli/helm/cli/kubectl.py diff --git a/scripts/helm/cli/py.typed b/src/app/cli/helm/cli/py.typed similarity index 100% rename from scripts/helm/cli/py.typed rename to src/app/cli/helm/cli/py.typed diff --git a/scripts/helm/helpers.py b/src/app/cli/helm/helpers.py similarity index 83% rename from scripts/helm/helpers.py rename to src/app/cli/helm/helpers.py index 9899dcc..bb517a9 100644 --- a/scripts/helm/helpers.py +++ b/src/app/cli/helm/helpers.py @@ -1,25 +1,8 @@ from decimal import Decimal -from pathlib import Path -from app.core.settings import Settings from kubernetes.utils import parse_quantity # type: ignore -from .models import K8sName - - -def resolve_namespace_context_and_kubeconfig( - cli_release_name_arg: str, - cli_namespace_arg: str, - cli_kube_context_arg: str, - cli_kubeconfig_arg: Path, - settings: Settings, -) -> tuple[K8sName, K8sName, str, str]: - """Resolves the effective namespace and Kubernetes context.""" - release_name_obj = K8sName(name=cli_release_name_arg) - namespace_obj = K8sName(name=cli_namespace_arg) - resolved_kubeconfig_str: str = str(cli_kubeconfig_arg.expanduser()) - - return release_name_obj, namespace_obj, cli_kube_context_arg, resolved_kubeconfig_str +from app.core.settings import Settings def format_memory(bytes_val: Decimal) -> str: diff --git a/scripts/helm/main.py b/src/app/cli/helm/main.py similarity index 65% rename from scripts/helm/main.py rename to src/app/cli/helm/main.py index a21c38e..fa865da 100644 --- a/scripts/helm/main.py +++ b/src/app/cli/helm/main.py @@ -7,12 +7,12 @@ from pathlib import Path import typer + from app.core.settings import Settings from .cli.helm import run_helm_command from .cli.kubectl import delete_namespace -from .helpers import map_config_to_helm_values, resolve_namespace_context_and_kubeconfig -from .models import HelmChartPath +from .helpers import map_config_to_helm_values @lru_cache() @@ -36,6 +36,7 @@ def deploy( # noqa: PLR0913 exists=True, dir_okay=True, file_okay=False, # Ensure it's a directory + readable=True, ), release_name: str = typer.Option( settings.kubernetes.SELENIUM_GRID_SERVICE_NAME, @@ -53,6 +54,9 @@ def deploy( # noqa: PLR0913 settings.kubernetes.KUBECONFIG, "--kubeconfig", help="Path to the kubeconfig file.", + exists=True, + file_okay=True, + dir_okay=False, ), debug: bool = typer.Option( False, @@ -60,27 +64,15 @@ def deploy( # noqa: PLR0913 ), ) -> None: """Deploy Selenium Grid using Helm CLI.""" - - # Validate inputs using Pydantic models - chart = HelmChartPath(path=chart_path) - - release_name_obj, namespace_obj, effective_kube_context, effective_kubeconfig = ( - resolve_namespace_context_and_kubeconfig( - cli_release_name_arg=release_name, - cli_namespace_arg=namespace, - cli_kube_context_arg=context, - cli_kubeconfig_arg=kubeconfig, - settings=settings, - ) - ) + kubeconfig_expanduser_str = str(kubeconfig.expanduser()) if debug: typer.echo("--- Debug Information ---") - typer.echo(f"Chart: {chart.path}") - typer.echo(f"Release Name: {release_name_obj}") - typer.echo(f"Namespace: {namespace_obj}") - typer.echo(f"Context: {effective_kube_context or 'Default'}") - typer.echo(f"kubeconfig: {effective_kubeconfig or 'Default'}") + typer.echo(f"Chart: {chart_path}") + typer.echo(f"Release Name: {release_name}") + typer.echo(f"Namespace: {namespace}") + typer.echo(f"Context: {context or 'Default'}") + typer.echo(f"kubeconfig: {kubeconfig_expanduser_str or 'Default'}") typer.echo("-------------------------") # Get Helm arguments @@ -101,12 +93,12 @@ def deploy( # noqa: PLR0913 "helm", "upgrade", "--install", - str(release_name_obj), - str(chart.path), + str(release_name), + str(chart_path), # Use --namespace to ensure release metadata is stored in the same namespace as resources # Use --create-namespace to ensure namespace exists before chart creates resources "--namespace", - str(namespace_obj), + str(namespace), "--create-namespace", ] @@ -115,12 +107,12 @@ def deploy( # noqa: PLR0913 cmd_args.extend(["-f", values_file_path]) # Add kubeconfig if specified - if effective_kubeconfig: - cmd_args.extend(["--kubeconfig", effective_kubeconfig]) + if kubeconfig_expanduser_str: + cmd_args.extend(["--kubeconfig", kubeconfig_expanduser_str]) # Add context if specified - if effective_kube_context: - cmd_args.extend(["--kube-context", effective_kube_context]) + if context: + cmd_args.extend(["--kube-context", context]) # Add all --set arguments for arg in set_args: @@ -128,13 +120,13 @@ def deploy( # noqa: PLR0913 run_helm_command( cmd_args=cmd_args, - kube_context=effective_kube_context, - kubeconfig=effective_kubeconfig, + kube_context=context, + kubeconfig=kubeconfig_expanduser_str, debug=debug, ) typer.echo( - f"Helm release '{release_name_obj}' deployed/upgraded successfully in namespace '{namespace_obj}'." + f"Helm release '{release_name}' deployed/upgraded successfully in namespace '{namespace}'." ) finally: if values_file_path: @@ -159,6 +151,9 @@ def uninstall( # noqa: PLR0913 settings.kubernetes.KUBECONFIG, "--kubeconfig", help="Path to the kubeconfig file.", + exists=True, + file_okay=True, + dir_okay=False, ), debug: bool = typer.Option( False, @@ -171,56 +166,48 @@ def uninstall( # noqa: PLR0913 ), ) -> None: """Uninstall Selenium Grid Helm release.""" - release_name_obj, namespace_obj, effective_kube_context, effective_kubeconfig = ( - resolve_namespace_context_and_kubeconfig( - cli_release_name_arg=release_name, - cli_namespace_arg=namespace, - cli_kube_context_arg=context, - cli_kubeconfig_arg=kubeconfig, - settings=get_settings(), - ) - ) + kubeconfig_expanduser_str = str(kubeconfig.expanduser()) if debug: typer.echo("--- Debug Information ---") - typer.echo(f"Release Name: {release_name_obj}") - typer.echo(f"Namespace: {namespace_obj}") - typer.echo(f"Context: {effective_kube_context or 'Default'}") - typer.echo(f"kubeconfig: {effective_kubeconfig or 'Default'}") + typer.echo(f"Release Name: {release_name}") + typer.echo(f"Namespace: {namespace}") + typer.echo(f"Context: {context or 'Default'}") + typer.echo(f"kubeconfig: {kubeconfig_expanduser_str or 'Default'}") typer.echo("-------------------------") # Build the Helm command cmd_args = [ "helm", "uninstall", - str(release_name_obj), + str(release_name), "--namespace", - str(namespace_obj), + str(namespace), ] - if effective_kubeconfig: - cmd_args.extend(["--kubeconfig", effective_kubeconfig]) + if kubeconfig_expanduser_str: + cmd_args.extend(["--kubeconfig", kubeconfig_expanduser_str]) # Add context if specified - if effective_kube_context: - cmd_args.extend(["--kube-context", effective_kube_context]) + if context: + cmd_args.extend(["--kube-context", context]) run_helm_command( cmd_args=cmd_args, - kube_context=effective_kube_context, - kubeconfig=effective_kubeconfig, + kube_context=context, + kubeconfig=kubeconfig_expanduser_str, debug=debug, ) typer.echo( - f"Helm release '{release_name_obj}' uninstalled successfully from namespace '{namespace_obj}'." + f"Helm release '{release_name}' uninstalled successfully from namespace '{namespace}'." ) if delete_ns: delete_namespace( - str(namespace_obj), - effective_kube_context, - effective_kubeconfig, + str(namespace), + context, + kubeconfig_expanduser_str, debug, ) diff --git a/src/app/cli/main.py b/src/app/cli/main.py new file mode 100644 index 0000000..6a74e31 --- /dev/null +++ b/src/app/cli/main.py @@ -0,0 +1,77 @@ +from importlib.metadata import version as metadata_version +from typing import Any, Callable, cast + +from fastapi_cli.cli import dev, run +from typer import Exit, Option, Typer, echo + +from .helm.main import create_application as create_helm_app +from .utils import with_app_path + +DOC_TITLE = "[bold green_yellow]MCP Selenium Grid CLI[/bold green_yellow] πŸš€" +DOC_DESC = """ +[pale_turquoise1]Model Context Protocol (MCP) server that enables AI Agents to request +and manage Selenium browser instances through a secure API.[/pale_turquoise1] + +[italic gold1]Perfect for your automated browser testing needs![/italic gold1] + +[link=https://github.com/Falamarcao/mcp-selenium-grid]https://github.com/Falamarcao/mcp-selenium-grid[/link] +""" + + +def version_callback(value: bool) -> None: + if value: + echo(f"mcp-selenium-grid v{metadata_version('mcp-selenium-grid')}") + raise Exit() + + +def create_application() -> Typer: + app = Typer( + name="mcp-selenium-grid", + help=f"{DOC_TITLE}\n{DOC_DESC}", + rich_help_panel="main", + rich_markup_mode="rich", + add_completion=False, + no_args_is_help=True, + pretty_exceptions_show_locals=False, + ) + + @app.callback() + def main( + version: bool = Option( + False, + "--version", + "-v", + help="Show the version and exit", + is_eager=True, + callback=version_callback, + ), + ) -> None: + """Main CLI callback (used only to hook version flag).""" + + # ── FastAPI Commands ── + fastapi_cli = Typer(help="Custom FastAPI CLI with limited commands.") + + for name, cmd, desc in [ + ("dev", dev, "Run the MCP Server in [bright_green]development[/bright_green] mode"), + ("run", run, "Run the MCP Server in [bright_green]production[/bright_green] mode"), + ]: + fastapi_cli.command(name=name, help=desc)(with_app_path(cast(Callable[..., Any], cmd))) + + app.add_typer(fastapi_cli, name="server", help="Run MCP FastAPI server", no_args_is_help=True) + + # ── Helm Commands ── + try: + helm_app = create_helm_app() + app.add_typer( + helm_app, + name="helm", + help=f"{DOC_TITLE}\n[pale_turquoise1]Manage Kubernetes deployments with Helm ☸️[/pale_turquoise1]", + no_args_is_help=True, + ) + except ImportError: + pass # Helm optional + + return app + + +app = create_application() diff --git a/src/app/cli/utils.py b/src/app/cli/utils.py new file mode 100644 index 0000000..8a11955 --- /dev/null +++ b/src/app/cli/utils.py @@ -0,0 +1,22 @@ +from functools import wraps +from importlib.util import find_spec +from pathlib import Path +from typing import Any, Callable + + +def resolve_module_path(module_name: str) -> Path: + spec = find_spec(module_name) + if spec is None or spec.origin is None: + raise ImportError(f"Cannot find module '{module_name}'") + return Path(spec.origin).resolve() + + +def with_app_path(fn: Callable[..., Any]) -> Callable[..., Any]: + """Inject the path to the FastAPI app into CLI kwargs.""" + + @wraps(fn) + def wrapper(*args: Any, **kwargs: Any) -> Any: + kwargs["path"] = resolve_module_path("app.main") + return fn(*args, **kwargs) + + return wrapper diff --git a/uv.lock b/uv.lock index 13023ca..e64a579 100644 --- a/uv.lock +++ b/uv.lock @@ -583,7 +583,7 @@ wheels = [ [[package]] name = "mcp-selenium-grid" -version = "0.1.0.dev1" +version = "0.1.0.dev2" source = { editable = "." } dependencies = [ { name = "docker" }, From afcbe29c01d9be12da748336b8df145d3fe78ca4 Mon Sep 17 00:00:00 2001 From: falamarcao Date: Wed, 6 Aug 2025 03:09:39 -0300 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20ci:=20add=20coverage?= =?UTF-8?q?=20check=20with=20margin=20tolerance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit β€’ πŸ› οΈ Replace strict coverage --fail-under with custom script β€’ πŸ“Š Use MIN_COVERAGE and MARGIN env vars for flexible thresholding β€’ 🚦 Fail only if coverage < (MIN_COVERAGE - COVERAGE_TOLERANCE_MARGIN), warn if within margin β€’ πŸ”§ Improve CI stability by allowing minor coverage fluctuations --- .github/workflows/1_tests.yml | 5 +- scripts/check_coverage.py | 101 ++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100755 scripts/check_coverage.py diff --git a/.github/workflows/1_tests.yml b/.github/workflows/1_tests.yml index 30a0494..2477ddc 100644 --- a/.github/workflows/1_tests.yml +++ b/.github/workflows/1_tests.yml @@ -14,6 +14,7 @@ permissions: {} # deny all by default env: UV_SYSTEM_PYTHON: 1 MIN_COVERAGE: 70 + COVERAGE_TOLERANCE_MARGIN: 5 jobs: filter: @@ -103,10 +104,8 @@ jobs: - name: Combine and report coverage run: | - uv run coverage combine uv run coverage report --format=markdown >> $GITHUB_STEP_SUMMARY - uv run coverage report --fail-under=$MIN_COVERAGE - + uv run ./scripts/check_coverage.py --format=html >> $GITHUB_STEP_SUMMARY check: name: Did all tests pass? if: always() diff --git a/scripts/check_coverage.py b/scripts/check_coverage.py new file mode 100755 index 0000000..83d7bf6 --- /dev/null +++ b/scripts/check_coverage.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +""" +Coverage check with thresholds, html or rich output. +""" + +import os +import sys +from io import StringIO +from typing import Optional + +from coverage import Coverage +from rich.console import Console +from rich.table import Table +from typer import Option, Typer + +console = Console() + + +def get_coverage() -> float: + files = [f for f in os.listdir(".") if f.startswith(".coverage.")] + if files: + console.print(f"πŸ”„ Combining {len(files)} coverage files...") + Coverage().combine() + if not os.path.exists(".coverage"): + console.print("[red]❌ No coverage data found[/red]") + sys.exit(1) + cov = Coverage() + cov.load() + return cov.report(file=StringIO()) + + +def html_summary(cov_pct: float, min_cov: float, margin: float) -> str: + allowed = min_cov - margin + msg = ( + f"❌ Coverage too low! Below allowed minimum ({allowed:.1f}%) 🚨" + if cov_pct < allowed + else "⚠️ Coverage below threshold but within margin." + if cov_pct < min_cov + else "βœ… Coverage meets the threshold. πŸŽ‰" + ) + return f"""\ +
+

πŸ§ͺ Coverage Check Summary πŸ“Š

+ + + + + + +
πŸ“ˆ MetricπŸ“Š Value
βœ… Total Coverage{cov_pct:.1f}%
🎯 Min Required{min_cov:.1f}%
⚠️ Allowed Margin{margin:.1f}%
+

{msg}

+
""" + + +def rich_summary(cov_pct: float, min_cov: float, margin: float) -> int: + allowed = min_cov - margin + table = Table(title="πŸ§ͺ Coverage Check Summary πŸ“Š", title_style="bold magenta") + table.add_column("πŸ“ˆ Metric", style="cyan", no_wrap=True) + table.add_column("πŸ“Š Value", style="green", justify="right") + table.add_row("βœ… Total Coverage", f"{cov_pct:.1f}%") + table.add_row("🎯 Min Required", f"{min_cov:.1f}%") + table.add_row("⚠️ Allowed Margin", f"{margin:.1f}%") + console.print(table) + if cov_pct < allowed: + console.print( + f"[bold red]❌ Coverage too low! Below allowed minimum ({allowed:.1f}%) 🚨[/bold red]" + ) + return 1 + elif cov_pct < min_cov: + console.print("[bold yellow]⚠️ Coverage below threshold but within margin.[/bold yellow]") + else: + console.print("[bold green]βœ… Coverage meets the threshold. πŸŽ‰[/bold green]") + return 0 + + +def create_application() -> Typer: + app = Typer() + + @app.command() + def check( + format: Optional[str] = Option( + "rich", "--format", "-f", help="Output format (rich or html)" + ), + ) -> None: + cov_pct = get_coverage() + min_cov = float(os.getenv("MIN_COVERAGE", "70")) + margin = float(os.getenv("COVERAGE_TOLERANCE_MARGIN", "5")) + + if format == "html": + print(html_summary(cov_pct, min_cov, margin)) + sys.exit(1 if cov_pct < min_cov - margin else 0) + else: + sys.exit(rich_summary(cov_pct, min_cov, margin)) + + return app + + +app = create_application() + +if __name__ == "__main__": + app() From a9bab56adfbadecc42d7bda3e7bda7acbba4188a Mon Sep 17 00:00:00 2001 From: falamarcao Date: Thu, 7 Aug 2025 02:03:47 -0300 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=8E=89=20feat(dev-tools):=20add=20ric?= =?UTF-8?q?h-coverage=20development=20script=20=E2=80=A2=20=F0=9F=93=9D=20?= =?UTF-8?q?Create=20rich-coverage=20runner=20script=20with=20uv=20script?= =?UTF-8?q?=20dependencies=20=E2=80=A2=20=F0=9F=94=84=20Update=20Tests=20G?= =?UTF-8?q?itHub=20Actions=20workflow=20to=20include=20new=20coverage=20sc?= =?UTF-8?q?ript=20=E2=80=A2=20=E2=AC=86=EF=B8=8F=20Update=20pre-commit=20h?= =?UTF-8?q?ook=20versions=20in=20.pre-commit-config.yaml=20=E2=80=A2=20?= =?UTF-8?q?=F0=9F=9A=AB=20Exclude=20development=20scripts=20from=20package?= =?UTF-8?q?=20build=20=E2=80=A2=20=F0=9F=92=A1=20Add=20documentation=20for?= =?UTF-8?q?=20running=20coverage=20reports=20during=20development?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/1.2_unit_tests.yml | 2 +- .github/workflows/1.3_docker_tests.yml | 2 +- .github/workflows/1.4_kubernetes_tests.yml | 2 +- .github/workflows/1_tests.yml | 10 +- .pre-commit-config.yaml | 4 +- CONTRIBUTING.md | 8 +- pyproject.toml | 11 +- scripts/check_coverage.py | 101 ------------ scripts/rich-coverage.py | 32 ++++ scripts/rich-coverage.py.lock | 171 +++++++++++++++++++++ scripts/rich_coverage/__init__.py | 5 + scripts/rich_coverage/__main__.py | 11 ++ scripts/rich_coverage/cli.py | 55 +++++++ scripts/rich_coverage/coverage_data.py | 77 ++++++++++ scripts/rich_coverage/reporting.py | 118 ++++++++++++++ scripts/rich_coverage/settings.py | 24 +++ scripts/rich_coverage/status.py | 70 +++++++++ uv.lock | 152 +++++++++--------- 18 files changed, 653 insertions(+), 202 deletions(-) delete mode 100755 scripts/check_coverage.py create mode 100644 scripts/rich-coverage.py create mode 100644 scripts/rich-coverage.py.lock create mode 100644 scripts/rich_coverage/__init__.py create mode 100644 scripts/rich_coverage/__main__.py create mode 100644 scripts/rich_coverage/cli.py create mode 100644 scripts/rich_coverage/coverage_data.py create mode 100644 scripts/rich_coverage/reporting.py create mode 100644 scripts/rich_coverage/settings.py create mode 100644 scripts/rich_coverage/status.py diff --git a/.github/workflows/1.2_unit_tests.yml b/.github/workflows/1.2_unit_tests.yml index d3e4a94..981b1ff 100644 --- a/.github/workflows/1.2_unit_tests.yml +++ b/.github/workflows/1.2_unit_tests.yml @@ -28,7 +28,7 @@ jobs: uv.lock - name: Install dependencies - run: uv sync --locked --all-groups --extra test --extra coverage + run: uv sync --locked --all-groups --extra test - name: Run code format run: uv run ruff format . --check diff --git a/.github/workflows/1.3_docker_tests.yml b/.github/workflows/1.3_docker_tests.yml index 5a122a7..0d553f0 100644 --- a/.github/workflows/1.3_docker_tests.yml +++ b/.github/workflows/1.3_docker_tests.yml @@ -30,7 +30,7 @@ jobs: uv.lock - name: Install dependencies - run: uv sync --locked --all-groups --extra test --extra coverage + run: uv sync --locked --all-groups --extra test - name: Run Docker integration & E2E tests with coverage run: uv run coverage run --parallel-mode -m pytest -m "integration or e2e" -k "docker" diff --git a/.github/workflows/1.4_kubernetes_tests.yml b/.github/workflows/1.4_kubernetes_tests.yml index 0f02726..394c298 100644 --- a/.github/workflows/1.4_kubernetes_tests.yml +++ b/.github/workflows/1.4_kubernetes_tests.yml @@ -58,7 +58,7 @@ jobs: echo "KUBERNETES__KUBECONFIG=$kube_config" >> $GITHUB_ENV - name: Install dependencies - run: uv sync --locked --all-groups --extra test --extra coverage + run: uv sync --locked --all-groups --extra test - name: Deploy Selenium Grid to KinD run: uv run mcp-selenium-grid helm deploy diff --git a/.github/workflows/1_tests.yml b/.github/workflows/1_tests.yml index 2477ddc..4caac86 100644 --- a/.github/workflows/1_tests.yml +++ b/.github/workflows/1_tests.yml @@ -96,16 +96,10 @@ jobs: with: enable-cache: true cache-dependency-glob: | - pyproject.toml - uv.lock - - - name: Install dependencies - run: uv sync --locked --extra coverage + scripts/rich-coverage.py.lock - name: Combine and report coverage - run: | - uv run coverage report --format=markdown >> $GITHUB_STEP_SUMMARY - uv run ./scripts/check_coverage.py --format=html >> $GITHUB_STEP_SUMMARY + run: uv run ./scripts/rich-coverage.py --format=html >> $GITHUB_STEP_SUMMARY # no need to install dependencies check: name: Did all tests pass? if: always() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 67af1dd..2e39b70 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.7.21 + rev: 0.8.5 hooks: - id: uv-lock - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.12.3 + rev: v0.12.7 hooks: - id: ruff args: [--fix] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23db6e8..cdbbf1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,7 +88,7 @@ git clone git@github.com:Falamarcao/mcp-selenium-grid.git cd mcp-selenium-grid # Create a virtual environment and install dev/test dependencies -uv sync --all-groups --extra test --extra coverage +uv sync --all-groups --extra test ``` ### 3. Kubernetes Setup (Optional) @@ -178,8 +178,8 @@ uv run pytest -m integration uv run pytest -m e2e # Run with coverage -uv run coverage run -m pytest -m unit -uv run coverage report +uv run scripts/rich-coverage.py +uv run scripts/rich-coverage.py --format=html ``` #### πŸ§ͺ CI & Workflow Testing @@ -211,7 +211,7 @@ uv run ruff clean # Clear ruff cache ## πŸ“¦ Dependency Management -- Install all dependencies: `uv sync --all-groups --extra test --extra coverage` +- Install all dependencies: `uv sync --all-groups --extra test` - Add a dependency: `uv add ` - Add a dev dependency: `uv add --dev` - Add a test dependency: `uv add --optional test` diff --git a/pyproject.toml b/pyproject.toml index f881d04..8292052 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ # AI Agents and contributors: Please assume uv is the default tool for all dependency and environment management in this project. # You do NOT need to ask the user if uv is being usedβ€”always use uv commands for installs, updates, and checks. # -# - Install all dependencies: `uv sync --all-groups --extra test --extra coverage` +# - Install all dependencies: `uv sync --all-groups --extra test` # - Add a dependency: `uv add ` # - Add a dev dependency: `uv add --dev` # - Add a test dependency: `uv add --optional test` @@ -57,10 +57,7 @@ test = [ "pytest-mock>=3.14.1", "pytest-asyncio>=1.0.0", # Parallel test execution "pytest-sugar>=1.0.0", -] - -coverage = [ - "coverage[toml]>=7.9.2", + "coverage>=7.10.2", ] [build-system] @@ -73,7 +70,7 @@ packages = ["src/app"] [tool.ruff] line-length = 100 target-version = "py312" -src = ["src/app", "src/tests"] +src = ["src/app", "src/tests", "scripts"] [tool.ruff.lint] extend-select = [ @@ -96,7 +93,7 @@ disallow_untyped_defs = true check_untyped_defs = true [[tool.mypy.overrides]] -module = ["fastapi_mcp", "sh"] +module = ["fastapi_mcp"] ignore_missing_imports = true [tool.pytest.ini_options] diff --git a/scripts/check_coverage.py b/scripts/check_coverage.py deleted file mode 100755 index 83d7bf6..0000000 --- a/scripts/check_coverage.py +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env python3 -""" -Coverage check with thresholds, html or rich output. -""" - -import os -import sys -from io import StringIO -from typing import Optional - -from coverage import Coverage -from rich.console import Console -from rich.table import Table -from typer import Option, Typer - -console = Console() - - -def get_coverage() -> float: - files = [f for f in os.listdir(".") if f.startswith(".coverage.")] - if files: - console.print(f"πŸ”„ Combining {len(files)} coverage files...") - Coverage().combine() - if not os.path.exists(".coverage"): - console.print("[red]❌ No coverage data found[/red]") - sys.exit(1) - cov = Coverage() - cov.load() - return cov.report(file=StringIO()) - - -def html_summary(cov_pct: float, min_cov: float, margin: float) -> str: - allowed = min_cov - margin - msg = ( - f"❌ Coverage too low! Below allowed minimum ({allowed:.1f}%) 🚨" - if cov_pct < allowed - else "⚠️ Coverage below threshold but within margin." - if cov_pct < min_cov - else "βœ… Coverage meets the threshold. πŸŽ‰" - ) - return f"""\ -
-

πŸ§ͺ Coverage Check Summary πŸ“Š

- - - - - - -
πŸ“ˆ MetricπŸ“Š Value
βœ… Total Coverage{cov_pct:.1f}%
🎯 Min Required{min_cov:.1f}%
⚠️ Allowed Margin{margin:.1f}%
-

{msg}

-
""" - - -def rich_summary(cov_pct: float, min_cov: float, margin: float) -> int: - allowed = min_cov - margin - table = Table(title="πŸ§ͺ Coverage Check Summary πŸ“Š", title_style="bold magenta") - table.add_column("πŸ“ˆ Metric", style="cyan", no_wrap=True) - table.add_column("πŸ“Š Value", style="green", justify="right") - table.add_row("βœ… Total Coverage", f"{cov_pct:.1f}%") - table.add_row("🎯 Min Required", f"{min_cov:.1f}%") - table.add_row("⚠️ Allowed Margin", f"{margin:.1f}%") - console.print(table) - if cov_pct < allowed: - console.print( - f"[bold red]❌ Coverage too low! Below allowed minimum ({allowed:.1f}%) 🚨[/bold red]" - ) - return 1 - elif cov_pct < min_cov: - console.print("[bold yellow]⚠️ Coverage below threshold but within margin.[/bold yellow]") - else: - console.print("[bold green]βœ… Coverage meets the threshold. πŸŽ‰[/bold green]") - return 0 - - -def create_application() -> Typer: - app = Typer() - - @app.command() - def check( - format: Optional[str] = Option( - "rich", "--format", "-f", help="Output format (rich or html)" - ), - ) -> None: - cov_pct = get_coverage() - min_cov = float(os.getenv("MIN_COVERAGE", "70")) - margin = float(os.getenv("COVERAGE_TOLERANCE_MARGIN", "5")) - - if format == "html": - print(html_summary(cov_pct, min_cov, margin)) - sys.exit(1 if cov_pct < min_cov - margin else 0) - else: - sys.exit(rich_summary(cov_pct, min_cov, margin)) - - return app - - -app = create_application() - -if __name__ == "__main__": - app() diff --git a/scripts/rich-coverage.py b/scripts/rich-coverage.py new file mode 100644 index 0000000..dec0fa8 --- /dev/null +++ b/scripts/rich-coverage.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +# /// script +# requires-python = ">=3.12" +# dependencies = [ +# "coverage", +# "rich", +# "typer", +# ] +# /// +# https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to-create-an-executable-file +""" +Runner script for rich_coverage module. + +This script exists to provide a convenient way to run the rich_coverage +module during development without including it in the project's build artifacts. + +It uses uv's script functionality to manage dependencies automatically and +adds the scripts directory to Python's path to enable module imports. + +Usage: + uv run scripts/rich-coverage.py + uv run ./scripts/rich-coverage.py --format=html + +Lock dependencies (automatic with pre-commit hook): + uv lock --script scripts/rich-coverage.py + +""" + +from rich_coverage import rich_coverage_cli + +if __name__ == "__main__": + rich_coverage_cli() diff --git a/scripts/rich-coverage.py.lock b/scripts/rich-coverage.py.lock new file mode 100644 index 0000000..573b997 --- /dev/null +++ b/scripts/rich-coverage.py.lock @@ -0,0 +1,171 @@ +version = 1 +revision = 2 +requires-python = ">=3.12" + +[manifest] +requirements = [ + { name = "coverage" }, + { name = "rich" }, + { name = "typer" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, + { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, + { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, + { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, + { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, + { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, + { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, +] + +[[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 = "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 = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "typer" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c5/8c/7d682431efca5fd290017663ea4588bf6f2c6aad085c7f108c5dbc316e70/typer-0.16.0.tar.gz", hash = "sha256:af377ffaee1dbe37ae9440cb4e8f11686ea5ce4e9bae01b84ae7c63b87f1dd3b", size = 102625, upload-time = "2025-05-26T14:30:31.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/42/3efaf858001d2c2913de7f354563e3a3a2f0decae3efe98427125a8f441e/typer-0.16.0-py3-none-any.whl", hash = "sha256:1f79bed11d4d02d4310e3c1b7ba594183bcedb0ac73b27a9e5f28f6fb5b98855", size = 46317, upload-time = "2025-05-26T14:30:30.523Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] diff --git a/scripts/rich_coverage/__init__.py b/scripts/rich_coverage/__init__.py new file mode 100644 index 0000000..e9bc3d8 --- /dev/null +++ b/scripts/rich_coverage/__init__.py @@ -0,0 +1,5 @@ +"""Rich Coverage - Beautiful code coverage reporting.""" + +from .cli import rich_coverage_cli + +__all__ = ["rich_coverage_cli"] diff --git a/scripts/rich_coverage/__main__.py b/scripts/rich_coverage/__main__.py new file mode 100644 index 0000000..976a730 --- /dev/null +++ b/scripts/rich_coverage/__main__.py @@ -0,0 +1,11 @@ +"""Entry point for rich_coverage module.""" + +from . import rich_coverage_cli + + +def main() -> None: + rich_coverage_cli() + + +if __name__ == "__main__": + main() diff --git a/scripts/rich_coverage/cli.py b/scripts/rich_coverage/cli.py new file mode 100644 index 0000000..c870b44 --- /dev/null +++ b/scripts/rich_coverage/cli.py @@ -0,0 +1,55 @@ +"""Command-line interface for coverage checking.""" + +from enum import Enum + +from typer import Exit, Option, Typer + +from .coverage_data import extract_report_data, get_total_coverage, load_coverage +from .reporting import display_html_report, display_rich_report +from .settings import load_thresholds +from .status import evaluate_coverage + + +class OutputFormat(str, Enum): + """Supported output formats.""" + + RICH = "rich" + HTML = "html" + + +def create_application() -> Typer: + """Create the CLI application.""" + app = Typer(help="Beautiful code coverage reporting tool.") + + def run_coverage_check(format: OutputFormat) -> None: + """Execute the coverage checking workflow.""" + thresholds = load_thresholds() + coverage = load_coverage() + report_data = extract_report_data(coverage) + total_coverage = get_total_coverage(report_data) + status = evaluate_coverage(total_coverage, thresholds) + + if format == OutputFormat.HTML: + display_html_report(coverage, thresholds, status, total_coverage) + else: + display_rich_report(report_data, thresholds, status) + + raise Exit(code=status.exit_code) + + @app.command() + def check( + format: OutputFormat = Option( + OutputFormat.RICH, + "--format", + "-f", + help="Output format.", + case_sensitive=False, + ), + ) -> None: + """Check code coverage against configured thresholds.""" + run_coverage_check(format) + + return app + + +rich_coverage_cli = create_application() diff --git a/scripts/rich_coverage/coverage_data.py b/scripts/rich_coverage/coverage_data.py new file mode 100644 index 0000000..af1c38f --- /dev/null +++ b/scripts/rich_coverage/coverage_data.py @@ -0,0 +1,77 @@ +"""Coverage data processing and analysis.""" + +from json import loads +from os import close, listdir, path, remove +from tempfile import mkstemp +from typing import Any + +from coverage import Coverage +from rich.console import Console +from typer import Exit + + +def load_coverage() -> Coverage: + """Load coverage data from available sources.""" + _combine_partial_reports() + _validate_coverage_exists() + + coverage = Coverage() + coverage.load() + return coverage + + +def extract_report_data(coverage: Coverage) -> dict[str, Any]: + """Extract structured report data from coverage object.""" + file_descriptor, temp_path = mkstemp(suffix=".json") + + try: + coverage.json_report(outfile=temp_path) + return _read_json_report(temp_path) + finally: + _cleanup_temp_file(file_descriptor, temp_path) + + +def get_total_coverage(report_data: dict[str, Any]) -> float: + """Extract total coverage percentage from report data.""" + return float(report_data["totals"]["percent_covered"]) + + +def _combine_partial_reports() -> None: + """Combine partial coverage reports if they exist.""" + if any(f.startswith(".coverage.") for f in listdir(".")): + Console().print("πŸ”„ Combining coverage files...") + Coverage().combine() + + +def _validate_coverage_exists() -> None: + """Ensure coverage data file exists.""" + if not path.exists(".coverage"): + Console().print("[red]❌ No coverage data found.[/red]") + raise Exit(code=1) + + +def _read_json_report(file_path: str) -> dict[str, Any]: + """Read and parse JSON coverage report.""" + with open(file_path, "r") as file: + content = file.read() + if not content: + return _empty_report_structure() + json_report: dict[str, Any] = loads(content) + return json_report + + +def _empty_report_structure() -> dict[str, Any]: + """Return empty report structure for edge cases.""" + return { + "files": {}, + "totals": {"percent_covered": 0.0, "num_statements": 0, "missing_lines": 0}, + } + + +def _cleanup_temp_file(file_descriptor: int, file_path: str) -> None: + """Safely remove temporary file and close descriptor.""" + try: + close(file_descriptor) + remove(file_path) + except OSError: + pass diff --git a/scripts/rich_coverage/reporting.py b/scripts/rich_coverage/reporting.py new file mode 100644 index 0000000..4c1a0d7 --- /dev/null +++ b/scripts/rich_coverage/reporting.py @@ -0,0 +1,118 @@ +"""Coverage report generation and display.""" + +from io import StringIO +from os import getenv +from textwrap import dedent +from typing import Any + +from coverage import Coverage +from rich.console import Console +from rich.markdown import Markdown +from rich.table import Table +from rich.text import Text + +from .settings import CoverageThresholds +from .status import Status, evaluate_file_coverage + +IN_GITHUB_ACTIONS = getenv("GITHUB_ACTIONS", "false").lower() == "true" + + +def display_rich_report( + report_data: dict[str, Any], thresholds: CoverageThresholds, status: Status +) -> None: + """Display comprehensive coverage report in terminal.""" + console = Console() + + _show_file_details(console, report_data, thresholds) + _show_summary(console, report_data, thresholds) + _show_final_status(console, status) + + +def display_html_report( + coverage: Coverage, thresholds: CoverageThresholds, status: Status, total_coverage: float +) -> None: + """Display HTML coverage report.""" + console = Console() + + _show_text_report(console, coverage, output_format="markdown") + _show_html_summary(console, total_coverage, thresholds, status) + + +def _show_file_details( + console: Console, report_data: dict[str, Any], thresholds: CoverageThresholds +) -> None: + """Display per-file coverage details.""" + table = Table(title=":bar_chart: Coverage Report", show_lines=True) + table.add_column("Name", style="bold cyan") + table.add_column("Stmts", style="dim", justify="right") + table.add_column("Miss", style="dim", justify="right") + table.add_column("Cover", style="bold", justify="right") + table.add_column("Status", justify="center") + + for filename, file_data in report_data.get("files", {}).items(): + summary = file_data["summary"] + percentage = summary["percent_covered"] + file_status = evaluate_file_coverage(percentage, thresholds) + + table.add_row( + filename, + str(summary["num_statements"]), + str(summary["missing_lines"]), + Text(f"{percentage:.0f}%", style=file_status.color), + file_status.emoji, + ) + + console.print(table) + + +def _show_summary( + console: Console, report_data: dict[str, Any], thresholds: CoverageThresholds +) -> None: + """Display coverage summary information.""" + table = Table(title="πŸ§ͺ Coverage Check Summary πŸ“Š", title_style="bold magenta") + table.add_column("πŸ“ˆ Metric", style="cyan") + table.add_column("πŸ“Š Value", style="green", justify="right") + + total_coverage = report_data["totals"]["percent_covered"] + table.add_row("βœ… Total Coverage", f"{total_coverage:.1f}%") + table.add_row("🎯 Min Required", f"{thresholds.minimum:.1f}%") + table.add_row("⚠️ Allowed Margin", f"{thresholds.tolerance:.1f}%") + + console.print(table) + + +def _show_final_status(console: Console, status: Status) -> None: + """Display final coverage status message.""" + console.print(f"[bold {status.color}]{status.message}[/bold {status.color}]") + + +def _show_text_report(console: Console, coverage: Coverage, output_format: str = "") -> None: + """Display standard text coverage report.""" + report_buffer = StringIO() + coverage.report(file=report_buffer, output_format=output_format) + content = report_buffer.getvalue() + + if IN_GITHUB_ACTIONS: + print(content) + else: + markdown = Markdown(content) + console.print(markdown) + + +def _show_html_summary( + console: Console, total_coverage: float, thresholds: CoverageThresholds, status: Status +) -> None: + """Display HTML summary of coverage results.""" + html = dedent(f"""\ +
+

πŸ§ͺ Coverage Check Summary πŸ“Š

+ + + + + + +
πŸ“ˆ MetricπŸ“Š Value
βœ… Total Coverage{total_coverage:.1f}%
🎯 Min Required{thresholds.minimum:.1f}%
⚠️ Allowed Margin{thresholds.tolerance:.1f}%
+

{status.message}

+
""") + console.print(html) diff --git a/scripts/rich_coverage/settings.py b/scripts/rich_coverage/settings.py new file mode 100644 index 0000000..4102f03 --- /dev/null +++ b/scripts/rich_coverage/settings.py @@ -0,0 +1,24 @@ +"""Coverage threshold settings.""" + +from dataclasses import dataclass +from os import getenv + + +@dataclass(frozen=True) +class CoverageThresholds: + """Immutable coverage threshold configuration.""" + + minimum: float + tolerance: float + + @property + def failure_limit(self) -> float: + """Calculate the minimum acceptable coverage.""" + return self.minimum - self.tolerance + + +def load_thresholds() -> CoverageThresholds: + """Load coverage thresholds from environment or use defaults.""" + minimum = float(getenv("MIN_COVERAGE", "70")) + tolerance = float(getenv("COVERAGE_TOLERANCE_MARGIN", "5")) + return CoverageThresholds(minimum, tolerance) diff --git a/scripts/rich_coverage/status.py b/scripts/rich_coverage/status.py new file mode 100644 index 0000000..f94d54f --- /dev/null +++ b/scripts/rich_coverage/status.py @@ -0,0 +1,70 @@ +"""Coverage status evaluation and reporting.""" + +from dataclasses import dataclass +from typing import NamedTuple + +from .settings import CoverageThresholds + + +@dataclass(frozen=True) +class Status: + """Immutable coverage status report.""" + + message: str + color: str + emoji: str + exit_code: int + + +class FileStatus(NamedTuple): + """Simple file status indicator.""" + + emoji: str + color: str + + +def evaluate_coverage(coverage_percentage: float, thresholds: CoverageThresholds) -> Status: + """Determine coverage status based on thresholds.""" + if coverage_percentage >= thresholds.minimum: + return _success_status() + + if coverage_percentage >= thresholds.failure_limit: + return _warning_status(thresholds) + + return _failure_status(thresholds) + + +def evaluate_file_coverage( + coverage_percentage: float, thresholds: CoverageThresholds +) -> FileStatus: + """Determine individual file coverage status.""" + if coverage_percentage >= thresholds.minimum: + return FileStatus("βœ…", "green") + + if coverage_percentage >= thresholds.failure_limit: + return FileStatus("⚠️", "yellow") + + return FileStatus("❌", "red") + + +def _success_status() -> Status: + """Create successful coverage status.""" + return Status( + message="βœ… Coverage meets the threshold. πŸŽ‰", color="green", emoji="βœ…", exit_code=0 + ) + + +def _warning_status(thresholds: CoverageThresholds) -> Status: + """Create warning coverage status.""" + return Status( + message="⚠️ Coverage below threshold but within margin.", + color="yellow", + emoji="⚠️", + exit_code=0, + ) + + +def _failure_status(thresholds: CoverageThresholds) -> Status: + """Create failure coverage status.""" + message = f"❌ Coverage too low! Below allowed minimum ({thresholds.failure_limit:.1f}%) 🚨" + return Status(message=message, color="red", emoji="❌", exit_code=1) diff --git a/uv.lock b/uv.lock index e64a579..43c8084 100644 --- a/uv.lock +++ b/uv.lock @@ -13,16 +13,16 @@ wheels = [ [[package]] name = "anyio" -version = "4.9.0" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "sniffio" }, { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, ] [[package]] @@ -45,11 +45,11 @@ wheels = [ [[package]] name = "certifi" -version = "2025.7.14" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981, upload-time = "2025-07-14T03:29:28.449Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -98,14 +98,14 @@ wheels = [ [[package]] name = "click" -version = "8.2.2" +version = "8.2.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e9/87/105111999772ec9730e3d4d910c723ea9763ece2ec441533a5cea1e87e3c/click-8.2.2.tar.gz", hash = "sha256:068616e6ef9705a07b6db727cb9c248f4eb9dae437a30239f56fa94b18b852ef", size = 263977, upload-time = "2025-08-02T02:23:41.102Z" } +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/ec/85/e7297e34133ae1cfde3bffd30c24e1ef055248251baa877834e048687a28/click-8.2.2-py3-none-any.whl", hash = "sha256:52e1e9f5d3db8c85aa76968c7c67ed41ddbacb167f43201511c8fd61eb5ba2ca", size = 103900, upload-time = "2025-08-02T02:23:39.299Z" }, + { 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]] @@ -119,66 +119,66 @@ wheels = [ [[package]] name = "coverage" -version = "7.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/87/0e/66dbd4c6a7f0758a8d18044c048779ba21fb94856e1edcf764bd5403e710/coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57", size = 819938, upload-time = "2025-07-27T14:13:39.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a5/3f/b051feeb292400bd22d071fdf933b3ad389a8cef5c80c7866ed0c7414b9e/coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4", size = 214934, upload-time = "2025-07-27T14:11:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e4/a61b27d5c4c2d185bdfb0bfe9d15ab4ac4f0073032665544507429ae60eb/coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e", size = 215173, upload-time = "2025-07-27T14:11:38.005Z" }, - { url = "https://files.pythonhosted.org/packages/8a/01/40a6ee05b60d02d0bc53742ad4966e39dccd450aafb48c535a64390a3552/coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4", size = 246190, upload-time = "2025-07-27T14:11:39.887Z" }, - { url = "https://files.pythonhosted.org/packages/11/ef/a28d64d702eb583c377255047281305dc5a5cfbfb0ee36e721f78255adb6/coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a", size = 248618, upload-time = "2025-07-27T14:11:41.841Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ad/73d018bb0c8317725370c79d69b5c6e0257df84a3b9b781bda27a438a3be/coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe", size = 250081, upload-time = "2025-07-27T14:11:43.705Z" }, - { url = "https://files.pythonhosted.org/packages/2d/dd/496adfbbb4503ebca5d5b2de8bed5ec00c0a76558ffc5b834fd404166bc9/coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386", size = 247990, upload-time = "2025-07-27T14:11:45.244Z" }, - { url = "https://files.pythonhosted.org/packages/18/3c/a9331a7982facfac0d98a4a87b36ae666fe4257d0f00961a3a9ef73e015d/coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6", size = 246191, upload-time = "2025-07-27T14:11:47.093Z" }, - { url = "https://files.pythonhosted.org/packages/62/0c/75345895013b83f7afe92ec595e15a9a525ede17491677ceebb2ba5c3d85/coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f", size = 247400, upload-time = "2025-07-27T14:11:48.643Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a9/98b268cfc5619ef9df1d5d34fee408ecb1542d9fd43d467e5c2f28668cd4/coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca", size = 217338, upload-time = "2025-07-27T14:11:50.258Z" }, - { url = "https://files.pythonhosted.org/packages/fe/31/22a5440e4d1451f253c5cd69fdcead65e92ef08cd4ec237b8756dc0b20a7/coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3", size = 218125, upload-time = "2025-07-27T14:11:52.034Z" }, - { url = "https://files.pythonhosted.org/packages/d6/2b/40d9f0ce7ee839f08a43c5bfc9d05cec28aaa7c9785837247f96cbe490b9/coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4", size = 216523, upload-time = "2025-07-27T14:11:53.965Z" }, - { url = "https://files.pythonhosted.org/packages/ef/72/135ff5fef09b1ffe78dbe6fcf1e16b2e564cd35faeacf3d63d60d887f12d/coverage-7.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebb08d0867c5a25dffa4823377292a0ffd7aaafb218b5d4e2e106378b1061e39", size = 214960, upload-time = "2025-07-27T14:11:55.959Z" }, - { url = "https://files.pythonhosted.org/packages/b1/aa/73a5d1a6fc08ca709a8177825616aa95ee6bf34d522517c2595484a3e6c9/coverage-7.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f32a95a83c2e17422f67af922a89422cd24c6fa94041f083dd0bb4f6057d0bc7", size = 215220, upload-time = "2025-07-27T14:11:57.899Z" }, - { url = "https://files.pythonhosted.org/packages/8d/40/3124fdd45ed3772a42fc73ca41c091699b38a2c3bd4f9cb564162378e8b6/coverage-7.10.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c4c746d11c8aba4b9f58ca8bfc6fbfd0da4efe7960ae5540d1a1b13655ee8892", size = 245772, upload-time = "2025-07-27T14:12:00.422Z" }, - { url = "https://files.pythonhosted.org/packages/42/62/a77b254822efa8c12ad59e8039f2bc3df56dc162ebda55e1943e35ba31a5/coverage-7.10.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f39edd52c23e5c7ed94e0e4bf088928029edf86ef10b95413e5ea670c5e92d7", size = 248116, upload-time = "2025-07-27T14:12:03.099Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/8101f062f472a3a6205b458d18ef0444a63ae5d36a8a5ed5dd0f6167f4db/coverage-7.10.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab6e19b684981d0cd968906e293d5628e89faacb27977c92f3600b201926b994", size = 249554, upload-time = "2025-07-27T14:12:04.668Z" }, - { url = "https://files.pythonhosted.org/packages/8f/7b/e51bc61573e71ff7275a4f167aecbd16cb010aefdf54bcd8b0a133391263/coverage-7.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5121d8cf0eacb16133501455d216bb5f99899ae2f52d394fe45d59229e6611d0", size = 247766, upload-time = "2025-07-27T14:12:06.234Z" }, - { url = "https://files.pythonhosted.org/packages/4b/71/1c96d66a51d4204a9d6d12df53c4071d87e110941a2a1fe94693192262f5/coverage-7.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df1c742ca6f46a6f6cbcaef9ac694dc2cb1260d30a6a2f5c68c5f5bcfee1cfd7", size = 245735, upload-time = "2025-07-27T14:12:08.305Z" }, - { url = "https://files.pythonhosted.org/packages/13/d5/efbc2ac4d35ae2f22ef6df2ca084c60e13bd9378be68655e3268c80349ab/coverage-7.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:40f9a38676f9c073bf4b9194707aa1eb97dca0e22cc3766d83879d72500132c7", size = 247118, upload-time = "2025-07-27T14:12:09.903Z" }, - { url = "https://files.pythonhosted.org/packages/d1/22/073848352bec28ca65f2b6816b892fcf9a31abbef07b868487ad15dd55f1/coverage-7.10.1-cp313-cp313-win32.whl", hash = "sha256:2348631f049e884839553b9974f0821d39241c6ffb01a418efce434f7eba0fe7", size = 217381, upload-time = "2025-07-27T14:12:11.535Z" }, - { url = "https://files.pythonhosted.org/packages/b7/df/df6a0ff33b042f000089bd11b6bb034bab073e2ab64a56e78ed882cba55d/coverage-7.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:4072b31361b0d6d23f750c524f694e1a417c1220a30d3ef02741eed28520c48e", size = 218152, upload-time = "2025-07-27T14:12:13.182Z" }, - { url = "https://files.pythonhosted.org/packages/30/e3/5085ca849a40ed6b47cdb8f65471c2f754e19390b5a12fa8abd25cbfaa8f/coverage-7.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:3e31dfb8271937cab9425f19259b1b1d1f556790e98eb266009e7a61d337b6d4", size = 216559, upload-time = "2025-07-27T14:12:14.807Z" }, - { url = "https://files.pythonhosted.org/packages/cc/93/58714efbfdeb547909feaabe1d67b2bdd59f0597060271b9c548d5efb529/coverage-7.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1c4f679c6b573a5257af6012f167a45be4c749c9925fd44d5178fd641ad8bf72", size = 215677, upload-time = "2025-07-27T14:12:16.68Z" }, - { url = "https://files.pythonhosted.org/packages/c0/0c/18eaa5897e7e8cb3f8c45e563e23e8a85686b4585e29d53cacb6bc9cb340/coverage-7.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:871ebe8143da284bd77b84a9136200bd638be253618765d21a1fce71006d94af", size = 215899, upload-time = "2025-07-27T14:12:18.758Z" }, - { url = "https://files.pythonhosted.org/packages/84/c1/9d1affacc3c75b5a184c140377701bbf14fc94619367f07a269cd9e4fed6/coverage-7.10.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:998c4751dabf7d29b30594af416e4bf5091f11f92a8d88eb1512c7ba136d1ed7", size = 257140, upload-time = "2025-07-27T14:12:20.357Z" }, - { url = "https://files.pythonhosted.org/packages/3d/0f/339bc6b8fa968c346df346068cca1f24bdea2ddfa93bb3dc2e7749730962/coverage-7.10.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:780f750a25e7749d0af6b3631759c2c14f45de209f3faaa2398312d1c7a22759", size = 259005, upload-time = "2025-07-27T14:12:22.007Z" }, - { url = "https://files.pythonhosted.org/packages/c8/22/89390864b92ea7c909079939b71baba7e5b42a76bf327c1d615bd829ba57/coverage-7.10.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:590bdba9445df4763bdbebc928d8182f094c1f3947a8dc0fc82ef014dbdd8324", size = 261143, upload-time = "2025-07-27T14:12:23.746Z" }, - { url = "https://files.pythonhosted.org/packages/2c/56/3d04d89017c0c41c7a71bd69b29699d919b6bbf2649b8b2091240b97dd6a/coverage-7.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b2df80cb6a2af86d300e70acb82e9b79dab2c1e6971e44b78dbfc1a1e736b53", size = 258735, upload-time = "2025-07-27T14:12:25.73Z" }, - { url = "https://files.pythonhosted.org/packages/cb/40/312252c8afa5ca781063a09d931f4b9409dc91526cd0b5a2b84143ffafa2/coverage-7.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d6a558c2725bfb6337bf57c1cd366c13798bfd3bfc9e3dd1f4a6f6fc95a4605f", size = 256871, upload-time = "2025-07-27T14:12:27.767Z" }, - { url = "https://files.pythonhosted.org/packages/1f/2b/564947d5dede068215aaddb9e05638aeac079685101462218229ddea9113/coverage-7.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e6150d167f32f2a54690e572e0a4c90296fb000a18e9b26ab81a6489e24e78dd", size = 257692, upload-time = "2025-07-27T14:12:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/93/1b/c8a867ade85cb26d802aea2209b9c2c80613b9c122baa8c8ecea6799648f/coverage-7.10.1-cp313-cp313t-win32.whl", hash = "sha256:d946a0c067aa88be4a593aad1236493313bafaa27e2a2080bfe88db827972f3c", size = 218059, upload-time = "2025-07-27T14:12:31.076Z" }, - { url = "https://files.pythonhosted.org/packages/a1/fe/cd4ab40570ae83a516bf5e754ea4388aeedd48e660e40c50b7713ed4f930/coverage-7.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e37c72eaccdd5ed1130c67a92ad38f5b2af66eeff7b0abe29534225db2ef7b18", size = 219150, upload-time = "2025-07-27T14:12:32.746Z" }, - { url = "https://files.pythonhosted.org/packages/8d/16/6e5ed5854be6d70d0c39e9cb9dd2449f2c8c34455534c32c1a508c7dbdb5/coverage-7.10.1-cp313-cp313t-win_arm64.whl", hash = "sha256:89ec0ffc215c590c732918c95cd02b55c7d0f569d76b90bb1a5e78aa340618e4", size = 217014, upload-time = "2025-07-27T14:12:34.406Z" }, - { url = "https://files.pythonhosted.org/packages/54/8e/6d0bfe9c3d7121cf936c5f8b03e8c3da1484fb801703127dba20fb8bd3c7/coverage-7.10.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:166d89c57e877e93d8827dac32cedae6b0277ca684c6511497311249f35a280c", size = 214951, upload-time = "2025-07-27T14:12:36.069Z" }, - { url = "https://files.pythonhosted.org/packages/f2/29/e3e51a8c653cf2174c60532aafeb5065cea0911403fa144c9abe39790308/coverage-7.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bed4a2341b33cd1a7d9ffc47df4a78ee61d3416d43b4adc9e18b7d266650b83e", size = 215229, upload-time = "2025-07-27T14:12:37.759Z" }, - { url = "https://files.pythonhosted.org/packages/e0/59/3c972080b2fa18b6c4510201f6d4dc87159d450627d062cd9ad051134062/coverage-7.10.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddca1e4f5f4c67980533df01430184c19b5359900e080248bbf4ed6789584d8b", size = 245738, upload-time = "2025-07-27T14:12:39.453Z" }, - { url = "https://files.pythonhosted.org/packages/2e/04/fc0d99d3f809452654e958e1788454f6e27b34e43f8f8598191c8ad13537/coverage-7.10.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:37b69226001d8b7de7126cad7366b0778d36777e4d788c66991455ba817c5b41", size = 248045, upload-time = "2025-07-27T14:12:41.387Z" }, - { url = "https://files.pythonhosted.org/packages/5e/2e/afcbf599e77e0dfbf4c97197747250d13d397d27e185b93987d9eaac053d/coverage-7.10.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2f22102197bcb1722691296f9e589f02b616f874e54a209284dd7b9294b0b7f", size = 249666, upload-time = "2025-07-27T14:12:43.056Z" }, - { url = "https://files.pythonhosted.org/packages/6e/ae/bc47f7f8ecb7a06cbae2bf86a6fa20f479dd902bc80f57cff7730438059d/coverage-7.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1e0c768b0f9ac5839dac5cf88992a4bb459e488ee8a1f8489af4cb33b1af00f1", size = 247692, upload-time = "2025-07-27T14:12:44.83Z" }, - { url = "https://files.pythonhosted.org/packages/b6/26/cbfa3092d31ccba8ba7647e4d25753263e818b4547eba446b113d7d1efdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:991196702d5e0b120a8fef2664e1b9c333a81d36d5f6bcf6b225c0cf8b0451a2", size = 245536, upload-time = "2025-07-27T14:12:46.527Z" }, - { url = "https://files.pythonhosted.org/packages/56/77/9c68e92500e6a1c83d024a70eadcc9a173f21aadd73c4675fe64c9c43fdf/coverage-7.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae8e59e5f4fd85d6ad34c2bb9d74037b5b11be072b8b7e9986beb11f957573d4", size = 246954, upload-time = "2025-07-27T14:12:49.279Z" }, - { url = "https://files.pythonhosted.org/packages/7f/a5/ba96671c5a669672aacd9877a5987c8551501b602827b4e84256da2a30a7/coverage-7.10.1-cp314-cp314-win32.whl", hash = "sha256:042125c89cf74a074984002e165d61fe0e31c7bd40ebb4bbebf07939b5924613", size = 217616, upload-time = "2025-07-27T14:12:51.214Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3c/e1e1eb95fc1585f15a410208c4795db24a948e04d9bde818fe4eb893bc85/coverage-7.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:a22c3bfe09f7a530e2c94c87ff7af867259c91bef87ed2089cd69b783af7b84e", size = 218412, upload-time = "2025-07-27T14:12:53.429Z" }, - { url = "https://files.pythonhosted.org/packages/b0/85/7e1e5be2cb966cba95566ba702b13a572ca744fbb3779df9888213762d67/coverage-7.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:ee6be07af68d9c4fca4027c70cea0c31a0f1bc9cb464ff3c84a1f916bf82e652", size = 216776, upload-time = "2025-07-27T14:12:55.482Z" }, - { url = "https://files.pythonhosted.org/packages/62/0f/5bb8f29923141cca8560fe2217679caf4e0db643872c1945ac7d8748c2a7/coverage-7.10.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d24fb3c0c8ff0d517c5ca5de7cf3994a4cd559cde0315201511dbfa7ab528894", size = 215698, upload-time = "2025-07-27T14:12:57.225Z" }, - { url = "https://files.pythonhosted.org/packages/80/29/547038ffa4e8e4d9e82f7dfc6d152f75fcdc0af146913f0ba03875211f03/coverage-7.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1217a54cfd79be20512a67ca81c7da3f2163f51bbfd188aab91054df012154f5", size = 215902, upload-time = "2025-07-27T14:12:59.071Z" }, - { url = "https://files.pythonhosted.org/packages/e1/8a/7aaa8fbfaed900147987a424e112af2e7790e1ac9cd92601e5bd4e1ba60a/coverage-7.10.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:51f30da7a52c009667e02f125737229d7d8044ad84b79db454308033a7808ab2", size = 257230, upload-time = "2025-07-27T14:13:01.248Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1d/c252b5ffac44294e23a0d79dd5acf51749b39795ccc898faeabf7bee903f/coverage-7.10.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3718c757c82d920f1c94089066225ca2ad7f00bb904cb72b1c39ebdd906ccb", size = 259194, upload-time = "2025-07-27T14:13:03.247Z" }, - { url = "https://files.pythonhosted.org/packages/16/ad/6c8d9f83d08f3bac2e7507534d0c48d1a4f52c18e6f94919d364edbdfa8f/coverage-7.10.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc452481e124a819ced0c25412ea2e144269ef2f2534b862d9f6a9dae4bda17b", size = 261316, upload-time = "2025-07-27T14:13:04.957Z" }, - { url = "https://files.pythonhosted.org/packages/d6/4e/f9bbf3a36c061e2e0e0f78369c006d66416561a33d2bee63345aee8ee65e/coverage-7.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9d6f494c307e5cb9b1e052ec1a471060f1dea092c8116e642e7a23e79d9388ea", size = 258794, upload-time = "2025-07-27T14:13:06.715Z" }, - { url = "https://files.pythonhosted.org/packages/87/82/e600bbe78eb2cb0541751d03cef9314bcd0897e8eea156219c39b685f869/coverage-7.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fc0e46d86905ddd16b85991f1f4919028092b4e511689bbdaff0876bd8aab3dd", size = 256869, upload-time = "2025-07-27T14:13:08.933Z" }, - { url = "https://files.pythonhosted.org/packages/ce/5d/2fc9a9236c5268f68ac011d97cd3a5ad16cc420535369bedbda659fdd9b7/coverage-7.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80b9ccd82e30038b61fc9a692a8dc4801504689651b281ed9109f10cc9fe8b4d", size = 257765, upload-time = "2025-07-27T14:13:10.778Z" }, - { url = "https://files.pythonhosted.org/packages/8a/05/b4e00b2bd48a2dc8e1c7d2aea7455f40af2e36484ab2ef06deb85883e9fe/coverage-7.10.1-cp314-cp314t-win32.whl", hash = "sha256:e58991a2b213417285ec866d3cd32db17a6a88061a985dbb7e8e8f13af429c47", size = 218420, upload-time = "2025-07-27T14:13:12.882Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d21d05f33ea27ece327422240e69654b5932b0b29e7fbc40fbab3cf199bf/coverage-7.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:e88dd71e4ecbc49d9d57d064117462c43f40a21a1383507811cf834a4a620651", size = 219536, upload-time = "2025-07-27T14:13:14.718Z" }, - { url = "https://files.pythonhosted.org/packages/a6/68/7fea94b141281ed8be3d1d5c4319a97f2befc3e487ce33657fc64db2c45e/coverage-7.10.1-cp314-cp314t-win_arm64.whl", hash = "sha256:1aadfb06a30c62c2eb82322171fe1f7c288c80ca4156d46af0ca039052814bab", size = 217190, upload-time = "2025-07-27T14:13:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/0f/64/922899cff2c0fd3496be83fa8b81230f5a8d82a2ad30f98370b133c2c83b/coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7", size = 206597, upload-time = "2025-07-27T14:13:37.221Z" }, +version = "7.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/76/17780846fc7aade1e66712e1e27dd28faa0a5d987a1f433610974959eaa8/coverage-7.10.2.tar.gz", hash = "sha256:5d6e6d84e6dd31a8ded64759626627247d676a23c1b892e1326f7c55c8d61055", size = 820754, upload-time = "2025-08-04T00:35:17.511Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/1e/2c752bdbbf6f1199c59b1a10557fbb6fb3dc96b3c0077b30bd41a5922c1f/coverage-7.10.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:890ad3a26da9ec7bf69255b9371800e2a8da9bc223ae5d86daeb940b42247c83", size = 215311, upload-time = "2025-08-04T00:33:35.524Z" }, + { url = "https://files.pythonhosted.org/packages/68/6a/84277d73a2cafb96e24be81b7169372ba7ff28768ebbf98e55c85a491b0f/coverage-7.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38fd1ccfca7838c031d7a7874d4353e2f1b98eb5d2a80a2fe5732d542ae25e9c", size = 215550, upload-time = "2025-08-04T00:33:37.109Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e7/5358b73b46ac76f56cc2de921eeabd44fabd0b7ff82ea4f6b8c159c4d5dc/coverage-7.10.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:76c1ffaaf4f6f0f6e8e9ca06f24bb6454a7a5d4ced97a1bc466f0d6baf4bd518", size = 246564, upload-time = "2025-08-04T00:33:38.33Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0e/b0c901dd411cb7fc0cfcb28ef0dc6f3049030f616bfe9fc4143aecd95901/coverage-7.10.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:86da8a3a84b79ead5c7d0e960c34f580bc3b231bb546627773a3f53c532c2f21", size = 248993, upload-time = "2025-08-04T00:33:39.555Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4e/a876db272072a9e0df93f311e187ccdd5f39a190c6d1c1f0b6e255a0d08e/coverage-7.10.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99cef9731c8a39801830a604cc53c93c9e57ea8b44953d26589499eded9576e0", size = 250454, upload-time = "2025-08-04T00:33:41.023Z" }, + { url = "https://files.pythonhosted.org/packages/64/d6/1222dc69f8dd1be208d55708a9f4a450ad582bf4fa05320617fea1eaa6d8/coverage-7.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ea58b112f2966a8b91eb13f5d3b1f8bb43c180d624cd3283fb33b1cedcc2dd75", size = 248365, upload-time = "2025-08-04T00:33:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/62/e3/40fd71151064fc315c922dd9a35e15b30616f00146db1d6a0b590553a75a/coverage-7.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:20f405188d28da9522b7232e51154e1b884fc18d0b3a10f382d54784715bbe01", size = 246562, upload-time = "2025-08-04T00:33:43.663Z" }, + { url = "https://files.pythonhosted.org/packages/fc/14/8aa93ddcd6623ddaef5d8966268ac9545b145bce4fe7b1738fd1c3f0d957/coverage-7.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:64586ce42bbe0da4d9f76f97235c545d1abb9b25985a8791857690f96e23dc3b", size = 247772, upload-time = "2025-08-04T00:33:45.068Z" }, + { url = "https://files.pythonhosted.org/packages/07/4e/dcb1c01490623c61e2f2ea85cb185fa6a524265bb70eeb897d3c193efeb9/coverage-7.10.2-cp312-cp312-win32.whl", hash = "sha256:bc2e69b795d97ee6d126e7e22e78a509438b46be6ff44f4dccbb5230f550d340", size = 217710, upload-time = "2025-08-04T00:33:46.378Z" }, + { url = "https://files.pythonhosted.org/packages/79/16/e8aab4162b5f80ad2e5e1f54b1826e2053aa2f4db508b864af647f00c239/coverage-7.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:adda2268b8cf0d11f160fad3743b4dfe9813cd6ecf02c1d6397eceaa5b45b388", size = 218499, upload-time = "2025-08-04T00:33:48.048Z" }, + { url = "https://files.pythonhosted.org/packages/06/7f/c112ec766e8f1131ce8ce26254be028772757b2d1e63e4f6a4b0ad9a526c/coverage-7.10.2-cp312-cp312-win_arm64.whl", hash = "sha256:164429decd0d6b39a0582eaa30c67bf482612c0330572343042d0ed9e7f15c20", size = 217154, upload-time = "2025-08-04T00:33:49.299Z" }, + { url = "https://files.pythonhosted.org/packages/8d/04/9b7a741557f93c0ed791b854d27aa8d9fe0b0ce7bb7c52ca1b0f2619cb74/coverage-7.10.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:aca7b5645afa688de6d4f8e89d30c577f62956fefb1bad021490d63173874186", size = 215337, upload-time = "2025-08-04T00:33:50.61Z" }, + { url = "https://files.pythonhosted.org/packages/02/a4/8d1088cd644750c94bc305d3cf56082b4cdf7fb854a25abb23359e74892f/coverage-7.10.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:96e5921342574a14303dfdb73de0019e1ac041c863743c8fe1aa6c2b4a257226", size = 215596, upload-time = "2025-08-04T00:33:52.33Z" }, + { url = "https://files.pythonhosted.org/packages/01/2f/643a8d73343f70e162d8177a3972b76e306b96239026bc0c12cfde4f7c7a/coverage-7.10.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:11333094c1bff621aa811b67ed794865cbcaa99984dedea4bd9cf780ad64ecba", size = 246145, upload-time = "2025-08-04T00:33:53.641Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4a/722098d1848db4072cda71b69ede1e55730d9063bf868375264d0d302bc9/coverage-7.10.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6eb586fa7d2aee8d65d5ae1dd71414020b2f447435c57ee8de8abea0a77d5074", size = 248492, upload-time = "2025-08-04T00:33:55.366Z" }, + { url = "https://files.pythonhosted.org/packages/3f/b0/8a6d7f326f6e3e6ed398cde27f9055e860a1e858317001835c521673fb60/coverage-7.10.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2d358f259d8019d4ef25d8c5b78aca4c7af25e28bd4231312911c22a0e824a57", size = 249927, upload-time = "2025-08-04T00:33:57.042Z" }, + { url = "https://files.pythonhosted.org/packages/bb/21/1aaadd3197b54d1e61794475379ecd0f68d8fc5c2ebd352964dc6f698a3d/coverage-7.10.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5250bda76e30382e0a2dcd68d961afcab92c3a7613606e6269855c6979a1b0bb", size = 248138, upload-time = "2025-08-04T00:33:58.329Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/be75bafb2bdd22fd8bf9bf63cd5873b91bb26ec0d68f02d4b8b09c02decb/coverage-7.10.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a91e027d66eff214d88d9afbe528e21c9ef1ecdf4956c46e366c50f3094696d0", size = 246111, upload-time = "2025-08-04T00:33:59.899Z" }, + { url = "https://files.pythonhosted.org/packages/5e/30/a4f0c5e249c3cc60e6c6f30d8368e372f2d380eda40e0434c192ac27ccf5/coverage-7.10.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:228946da741558904e2c03ce870ba5efd9cd6e48cbc004d9a27abee08100a15a", size = 247493, upload-time = "2025-08-04T00:34:01.619Z" }, + { url = "https://files.pythonhosted.org/packages/85/99/f09b9493e44a75cf99ca834394c12f8cb70da6c1711ee296534f97b52729/coverage-7.10.2-cp313-cp313-win32.whl", hash = "sha256:95e23987b52d02e7c413bf2d6dc6288bd5721beb518052109a13bfdc62c8033b", size = 217756, upload-time = "2025-08-04T00:34:03.277Z" }, + { url = "https://files.pythonhosted.org/packages/2d/bb/cbcb09103be330c7d26ff0ab05c4a8861dd2e254656fdbd3eb7600af4336/coverage-7.10.2-cp313-cp313-win_amd64.whl", hash = "sha256:f35481d42c6d146d48ec92d4e239c23f97b53a3f1fbd2302e7c64336f28641fe", size = 218526, upload-time = "2025-08-04T00:34:04.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/8f/8bfb4e0bca52c00ab680767c0dd8cfd928a2a72d69897d9b2d5d8b5f63f5/coverage-7.10.2-cp313-cp313-win_arm64.whl", hash = "sha256:65b451949cb789c346f9f9002441fc934d8ccedcc9ec09daabc2139ad13853f7", size = 217176, upload-time = "2025-08-04T00:34:05.973Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/d458ba0bf16a8204a88d74dbb7ec5520f29937ffcbbc12371f931c11efd2/coverage-7.10.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:e8415918856a3e7d57a4e0ad94651b761317de459eb74d34cc1bb51aad80f07e", size = 216058, upload-time = "2025-08-04T00:34:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1c/af4dfd2d7244dc7610fed6d59d57a23ea165681cd764445dc58d71ed01a6/coverage-7.10.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f287a25a8ca53901c613498e4a40885b19361a2fe8fbfdbb7f8ef2cad2a23f03", size = 216273, upload-time = "2025-08-04T00:34:09.073Z" }, + { url = "https://files.pythonhosted.org/packages/8e/67/ec5095d4035c6e16368226fa9cb15f77f891194c7e3725aeefd08e7a3e5a/coverage-7.10.2-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:75cc1a3f8c88c69bf16a871dab1fe5a7303fdb1e9f285f204b60f1ee539b8fc0", size = 257513, upload-time = "2025-08-04T00:34:10.403Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/be5550b57a3a8ba797de4236b0fd31031f88397b2afc84ab3c2d4cf265f6/coverage-7.10.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ca07fa78cc9d26bc8c4740de1abd3489cf9c47cc06d9a8ab3d552ff5101af4c0", size = 259377, upload-time = "2025-08-04T00:34:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/50/b12a4da1382e672305c2d17cd3029dc16b8a0470de2191dbf26b91431378/coverage-7.10.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2e117e64c26300032755d4520cd769f2623cde1a1d1c3515b05a3b8add0ade1", size = 261516, upload-time = "2025-08-04T00:34:13.608Z" }, + { url = "https://files.pythonhosted.org/packages/db/41/4d3296dbd33dd8da178171540ca3391af7c0184c0870fd4d4574ac290290/coverage-7.10.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:daaf98009977f577b71f8800208f4d40d4dcf5c2db53d4d822787cdc198d76e1", size = 259110, upload-time = "2025-08-04T00:34:15.089Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/b409959ecbc0cec0e61e65683b22bacaa4a3b11512f834e16dd8ffbc37db/coverage-7.10.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ea8d8fe546c528535c761ba424410bbeb36ba8a0f24be653e94b70c93fd8a8ca", size = 257248, upload-time = "2025-08-04T00:34:16.501Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/7076dc1c240412e9267d36ec93e9e299d7659f6a5c1e958f87e998b0fb6d/coverage-7.10.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fe024d40ac31eb8d5aae70215b41dafa264676caa4404ae155f77d2fa95c37bb", size = 258063, upload-time = "2025-08-04T00:34:18.338Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/f6b51a0288f8f5f7dcc7c89abdd22cf514f3bc5151284f5cd628917f8e10/coverage-7.10.2-cp313-cp313t-win32.whl", hash = "sha256:8f34b09f68bdadec122ffad312154eda965ade433559cc1eadd96cca3de5c824", size = 218433, upload-time = "2025-08-04T00:34:19.71Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6d/547a86493e25270ce8481543e77f3a0aa3aa872c1374246b7b76273d66eb/coverage-7.10.2-cp313-cp313t-win_amd64.whl", hash = "sha256:71d40b3ac0f26fa9ffa6ee16219a714fed5c6ec197cdcd2018904ab5e75bcfa3", size = 219523, upload-time = "2025-08-04T00:34:21.171Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d5/3c711e38eaf9ab587edc9bed232c0298aed84e751a9f54aaa556ceaf7da6/coverage-7.10.2-cp313-cp313t-win_arm64.whl", hash = "sha256:abb57fdd38bf6f7dcc66b38dafb7af7c5fdc31ac6029ce373a6f7f5331d6f60f", size = 217739, upload-time = "2025-08-04T00:34:22.514Z" }, + { url = "https://files.pythonhosted.org/packages/71/53/83bafa669bb9d06d4c8c6a055d8d05677216f9480c4698fb183ba7ec5e47/coverage-7.10.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a3e853cc04987c85ec410905667eed4bf08b1d84d80dfab2684bb250ac8da4f6", size = 215328, upload-time = "2025-08-04T00:34:23.991Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6c/30827a9c5a48a813e865fbaf91e2db25cce990bd223a022650ef2293fe11/coverage-7.10.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0100b19f230df72c90fdb36db59d3f39232391e8d89616a7de30f677da4f532b", size = 215608, upload-time = "2025-08-04T00:34:25.437Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a0/c92d85948056ddc397b72a3d79d36d9579c53cb25393ed3c40db7d33b193/coverage-7.10.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9c1cd71483ea78331bdfadb8dcec4f4edfb73c7002c1206d8e0af6797853f5be", size = 246111, upload-time = "2025-08-04T00:34:26.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/d695cf86b2559aadd072c91720a7844be4fb82cb4a3b642a2c6ce075692d/coverage-7.10.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9f75dbf4899e29a37d74f48342f29279391668ef625fdac6d2f67363518056a1", size = 248419, upload-time = "2025-08-04T00:34:28.726Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0a/03206aec4a05986e039418c038470d874045f6e00426b0c3879adc1f9251/coverage-7.10.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7df481e7508de1c38b9b8043da48d94931aefa3e32b47dd20277e4978ed5b95", size = 250038, upload-time = "2025-08-04T00:34:30.061Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9b/b3bd6bd52118c12bc4cf319f5baba65009c9beea84e665b6b9f03fa3f180/coverage-7.10.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:835f39e618099325e7612b3406f57af30ab0a0af350490eff6421e2e5f608e46", size = 248066, upload-time = "2025-08-04T00:34:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/80/cc/bfa92e261d3e055c851a073e87ba6a3bff12a1f7134233e48a8f7d855875/coverage-7.10.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:12e52b5aa00aa720097d6947d2eb9e404e7c1101ad775f9661ba165ed0a28303", size = 245909, upload-time = "2025-08-04T00:34:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/c8df15db4847710c72084164f615ae900af1ec380dce7f74a5678ccdf5e1/coverage-7.10.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:718044729bf1fe3e9eb9f31b52e44ddae07e434ec050c8c628bf5adc56fe4bdd", size = 247329, upload-time = "2025-08-04T00:34:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/04/6f/cb66e1f7124d5dd9ced69f889f02931419cb448125e44a89a13f4e036124/coverage-7.10.2-cp314-cp314-win32.whl", hash = "sha256:f256173b48cc68486299d510a3e729a96e62c889703807482dbf56946befb5c8", size = 218007, upload-time = "2025-08-04T00:34:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e1/3d4be307278ce32c1b9d95cc02ee60d54ddab784036101d053ec9e4fe7f5/coverage-7.10.2-cp314-cp314-win_amd64.whl", hash = "sha256:2e980e4179f33d9b65ac4acb86c9c0dde904098853f27f289766657ed16e07b3", size = 218802, upload-time = "2025-08-04T00:34:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/ec/66/1e43bbeb66c55a5a5efec70f1c153cf90cfc7f1662ab4ebe2d844de9122c/coverage-7.10.2-cp314-cp314-win_arm64.whl", hash = "sha256:14fb5b6641ab5b3c4161572579f0f2ea8834f9d3af2f7dd8fbaecd58ef9175cc", size = 217397, upload-time = "2025-08-04T00:34:39.15Z" }, + { url = "https://files.pythonhosted.org/packages/81/01/ae29c129217f6110dc694a217475b8aecbb1b075d8073401f868c825fa99/coverage-7.10.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e96649ac34a3d0e6491e82a2af71098e43be2874b619547c3282fc11d3840a4b", size = 216068, upload-time = "2025-08-04T00:34:40.648Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/6e9221d4139f357258f36dfa1d8cac4ec56d9d5acf5fdcc909bb016954d7/coverage-7.10.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1a2e934e9da26341d342d30bfe91422bbfdb3f1f069ec87f19b2909d10d8dcc4", size = 216285, upload-time = "2025-08-04T00:34:42.441Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ec/89d1d0c0ece0d296b4588e0ef4df185200456d42a47f1141335f482c2fc5/coverage-7.10.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:651015dcd5fd9b5a51ca79ece60d353cacc5beaf304db750407b29c89f72fe2b", size = 257603, upload-time = "2025-08-04T00:34:43.899Z" }, + { url = "https://files.pythonhosted.org/packages/82/06/c830af66734671c778fc49d35b58339e8f0687fbd2ae285c3f96c94da092/coverage-7.10.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81bf6a32212f9f66da03d63ecb9cd9bd48e662050a937db7199dbf47d19831de", size = 259568, upload-time = "2025-08-04T00:34:45.519Z" }, + { url = "https://files.pythonhosted.org/packages/60/57/f280dd6f1c556ecc744fbf39e835c33d3ae987d040d64d61c6f821e87829/coverage-7.10.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d800705f6951f75a905ea6feb03fff8f3ea3468b81e7563373ddc29aa3e5d1ca", size = 261691, upload-time = "2025-08-04T00:34:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/54/2b/c63a0acbd19d99ec32326164c23df3a4e18984fb86e902afdd66ff7b3d83/coverage-7.10.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:248b5394718e10d067354448dc406d651709c6765669679311170da18e0e9af8", size = 259166, upload-time = "2025-08-04T00:34:48.792Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c5/cd2997dcfcbf0683634da9df52d3967bc1f1741c1475dd0e4722012ba9ef/coverage-7.10.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:5c61675a922b569137cf943770d7ad3edd0202d992ce53ac328c5ff68213ccf4", size = 257241, upload-time = "2025-08-04T00:34:51.038Z" }, + { url = "https://files.pythonhosted.org/packages/16/26/c9e30f82fdad8d47aee90af4978b18c88fa74369ae0f0ba0dbf08cee3a80/coverage-7.10.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:52d708b5fd65589461381fa442d9905f5903d76c086c6a4108e8e9efdca7a7ed", size = 258139, upload-time = "2025-08-04T00:34:52.533Z" }, + { url = "https://files.pythonhosted.org/packages/c9/99/bdb7bd00bebcd3dedfb895fa9af8e46b91422993e4a37ac634a5f1113790/coverage-7.10.2-cp314-cp314t-win32.whl", hash = "sha256:916369b3b914186b2c5e5ad2f7264b02cff5df96cdd7cdad65dccd39aa5fd9f0", size = 218809, upload-time = "2025-08-04T00:34:54.075Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5e/56a7852e38a04d1520dda4dfbfbf74a3d6dec932c20526968f7444763567/coverage-7.10.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5b9d538e8e04916a5df63052d698b30c74eb0174f2ca9cd942c981f274a18eaf", size = 219926, upload-time = "2025-08-04T00:34:55.643Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/7fbe6b9c52bb9d627e9556f9f2edfdbe88b315e084cdecc9afead0c3b36a/coverage-7.10.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04c74f9ef1f925456a9fd23a7eef1103126186d0500ef9a0acb0bd2514bdc7cc", size = 217925, upload-time = "2025-08-04T00:34:57.564Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/9b768ac73a8ac2d10c080af23937212434a958c8d2a1c84e89b450237942/coverage-7.10.2-py3-none-any.whl", hash = "sha256:95db3750dd2e6e93d99fa2498f3a1580581e49c494bddccc6f85c5c21604921f", size = 206973, upload-time = "2025-08-04T00:35:15.918Z" }, ] [[package]] @@ -602,10 +602,8 @@ dependencies = [ ] [package.optional-dependencies] -coverage = [ - { name = "coverage" }, -] test = [ + { name = "coverage" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-mock" }, @@ -625,7 +623,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "coverage", extras = ["toml"], marker = "extra == 'coverage'", specifier = ">=7.9.2" }, + { name = "coverage", marker = "extra == 'test'", specifier = ">=7.10.2" }, { name = "docker", specifier = ">=7.1.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.14" }, { name = "fastapi-cli", extras = ["standard-no-fastapi-cloud-cli"], specifier = ">=0.0.8" }, @@ -644,7 +642,7 @@ requires-dist = [ { name = "typer", specifier = ">=0.16.0" }, { name = "uvicorn", specifier = ">=0.35.0" }, ] -provides-extras = ["test", "coverage"] +provides-extras = ["test"] [package.metadata.requires-dev] dev = [ @@ -1511,16 +1509,16 @@ wheels = [ [[package]] name = "virtualenv" -version = "20.32.0" +version = "20.33.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/96/0834f30fa08dca3738614e6a9d42752b6420ee94e58971d702118f7cfd30/virtualenv-20.32.0.tar.gz", hash = "sha256:886bf75cadfdc964674e6e33eb74d787dff31ca314ceace03ca5810620f4ecf0", size = 6076970, upload-time = "2025-07-21T04:09:50.985Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/60/4f20960df6c7b363a18a55ab034c8f2bcd5d9770d1f94f9370ec104c1855/virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8", size = 6082160, upload-time = "2025-08-05T16:10:55.605Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/c6/f8f28009920a736d0df434b52e9feebfb4d702ba942f15338cb4a83eafc1/virtualenv-20.32.0-py3-none-any.whl", hash = "sha256:2c310aecb62e5aa1b06103ed7c2977b81e042695de2697d01017ff0f1034af56", size = 6057761, upload-time = "2025-07-21T04:09:48.059Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ff/ded57ac5ff40a09e6e198550bab075d780941e0b0f83cbeabd087c59383a/virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67", size = 6060362, upload-time = "2025-08-05T16:10:52.81Z" }, ] [[package]]