Team Ai
Apppublic

openenv/echo_env

sourceHugging Faceupdated 1d agoView on Hugging Face
6likes
validate.py265 linesDownload Raw Back to commands
1# SPDX-License-Identifier: BSD-3-Clause2 3"""OpenEnv validate command."""4 5import json6from pathlib import Path7from typing import Annotated, Any8 9import typer10from openenv.cli._validation import validate_running_environment11from openenv.validation import (12    Level,13    load_policy,14    PolicyError,15    run_validation,16    SignatureError,17    UnsupportedPackageError,18    ValidationReport,19    Verdict,20    write_report,21)22 23 24EXIT_PASS = 025EXIT_FAIL = 126EXIT_UNSUPPORTED = 227EXIT_INTERNAL = 328 29_LEVELS = {30    "static": Level.STATIC,31    "runtime": Level.RUNTIME,32    "semantic": Level.SEMANTIC,33}34 35 36def _looks_like_url(value: str) -> bool:37    candidate = value.strip().lower()38    return candidate.startswith("http://") or candidate.startswith("https://")39 40 41def _render_report(report: ValidationReport | dict[str, Any]) -> str:42    if isinstance(report, dict):43        lines = [44            f"Validation report for {report.get('target', '')} (profile: {report.get('standard_profile', 'running_environment')})",45            f"  standard version: {report.get('standard_version', 'unknown')} · mode: {report.get('mode', 'unknown')}",46        ]47        for criterion in report.get("criteria", []):48            status = "PASS " if criterion.get("passed") else "FAIL "49            check_id = criterion.get("id", "")50            lines.append(f"  {status} {check_id}")51            if not criterion.get("passed"):52                details = criterion.get("details")53                if details:54                    lines.append(f"          {details}")55                expected = criterion.get("expected")56                if expected is not None:57                    lines.append(f"          expected: {expected}")58                actual = criterion.get("actual")59                if actual is not None:60                    lines.append(f"          actual: {actual}")61        verdict = "PASS" if report.get("passed", False) else "FAIL"62        lines.append(f"Verdict: {verdict}")63        return "\n".join(lines)64 65    lines = [66        f"Validation report for {report.target} (signature: {report.signature.value})",67        f"  policy {report.policy_version} · levels run: "68        + ", ".join(level.name.lower() for level in report.levels_run),69    ]70    for result in report.results:71        lines.append(72            f"  {result.status.value.upper():5s} {result.check_id} ({result.duration_s:.2f}s)"73        )74        if result.status.value in ("fail", "error", "skip"):75            for line in result.evidence:76                lines.append(f"          {line}")77            if result.remediation:78                lines.append(f"          remediation: {result.remediation}")79    lines.append(f"Verdict: {report.verdict.value.upper()}")80    return "\n".join(lines)81 82 83def _write_runtime_report(report: dict[str, Any], path: Path | None = None) -> str:84    payload = json.dumps(report, indent=2)85    if path is not None:86        path.write_text(payload + "\n")87    return payload88 89 90def validate(91    target: Annotated[92        str | None,93        typer.Argument(94            help=(95                "Path to the package directory (default: current directory) "96                "or a running OpenEnv URL (http://... or https://...)"97            ),98        ),99    ] = None,100    url: Annotated[101        str | None,102        typer.Option(103            "--url",104            help="Validate a running OpenEnv server by base URL (e.g. http://localhost:8000)",105        ),106    ] = None,107    level: Annotated[108        str,109        typer.Option(110            "--level",111            help="Validation level ceiling: static, runtime, or semantic",112        ),113    ] = "semantic",114    skip_build: Annotated[115        bool,116        typer.Option(117            "--skip-build",118            help="Skip the image build; build-dependent checks are SKIPped with a reason",119        ),120    ] = False,121    local: Annotated[122        bool,123        typer.Option(124            "--local",125            help="Explicitly select the default local-package mode (incompatible with --url)",126        ),127    ] = False,128    policy_version: Annotated[129        str | None,130        typer.Option(131            "--policy", help="Severity policy version (static: v1; runtime: v2)"132        ),133    ] = None,134    json_output: Annotated[135        bool,136        typer.Option("--json", help="Print the validation report as JSON"),137    ] = False,138    output: Annotated[139        Path | None,140        typer.Option("--output", help="Write the JSON report to a file"),141    ] = None,142    timeout: Annotated[143        float,144        typer.Option(145            "--timeout",146            help="HTTP timeout in seconds for --url runtime validation",147            min=0.1,148        ),149    ] = 5.0,150) -> None:151    """152    Validate a local package or a running OpenEnv server.153 154    Local validation detects the package format by its well-known file (the155    formats this build can parse; currently `openenv.yaml`), parses it into the156    normalized manifest, runs the applicable graders up to the requested level,157    applies the severity policy, and emits a report.158 159    Exit codes: 0 pass/warn · 1 fail · 2 unrecognized/unsupported package · 3160    internal or policy error.161 162    Examples:163 164    ```bash165    # Validate the current directory up to the semantic level166    openenv validate167 168    # Fast inner loop: static checks only, no image build169    openenv validate envs/echo_env --level static --skip-build170 171    # Machine-readable report172    openenv validate envs/echo_env --json173 174    # Probe a running server (legacy runtime probe)175    openenv validate --url http://localhost:8000176    ```177    """178    runtime_target = url179    if (180        runtime_target is not None181        and target is not None182        and not _looks_like_url(target)183    ):184        typer.echo(185            "Error: Cannot combine a local path argument with --url runtime validation",186            err=True,187        )188        raise typer.Exit(EXIT_FAIL)189 190    if target is not None and _looks_like_url(target):191        if runtime_target is not None and runtime_target != target:192            typer.echo(193                "Error: Conflicting runtime targets provided via argument and --url",194                err=True,195            )196            raise typer.Exit(EXIT_FAIL)197        runtime_target = target198 199    if runtime_target is not None:200        if local:201            typer.echo("Error: --local cannot be combined with a running URL", err=True)202            raise typer.Exit(EXIT_FAIL)203        try:204            report = validate_running_environment(runtime_target, timeout_s=timeout)205        except ValueError as exc:206            typer.echo(f"Error: {exc}", err=True)207            raise typer.Exit(EXIT_FAIL) from exc208 209        try:210            report_json = _write_runtime_report(report, output)211        except Exception as exc:212            typer.echo(f"Internal error: {exc}", err=True)213            raise typer.Exit(EXIT_INTERNAL) from exc214        if json_output:215            typer.echo(report_json)216        else:217            typer.echo(_render_report(report))218 219        if not report.get("passed", False):220            raise typer.Exit(EXIT_FAIL)221        return222 223    if level not in _LEVELS:224        typer.echo(225            f"Error: unknown level {level!r}; expected one of {sorted(_LEVELS)}",226            err=True,227        )228        raise typer.Exit(EXIT_INTERNAL)229 230    package_root = Path(target) if target is not None else Path.cwd()231    if not package_root.is_dir():232        typer.echo(f"Error: not a package directory: {package_root}", err=True)233        raise typer.Exit(EXIT_UNSUPPORTED)234 235    try:236        if output is not None and not output.parent.is_dir():237            raise OSError("report output directory does not exist")238        validation_report = run_validation(239            package_root,240            max_level=_LEVELS[level],241            skip_build=skip_build,242            policy=load_policy(policy_version) if policy_version else None,243            artifacts_dir=(244                output.parent / (output.stem + ".artifacts") if output else None245            ),246        )247        report_json = write_report(validation_report, output)248    except (SignatureError, UnsupportedPackageError) as exc:249        typer.echo(f"Error: {exc}", err=True)250        raise typer.Exit(EXIT_UNSUPPORTED) from exc251    except PolicyError as exc:252        typer.echo(f"Internal error: {exc}", err=True)253        raise typer.Exit(EXIT_INTERNAL) from exc254    except Exception as exc:255        typer.echo(f"Internal error: {exc}", err=True)256        raise typer.Exit(EXIT_INTERNAL) from exc257 258    if json_output:259        typer.echo(report_json)260    else:261        typer.echo(_render_report(validation_report))262 263    if validation_report.verdict is Verdict.FAIL:264        raise typer.Exit(EXIT_FAIL)265