Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| OpenEnv Submission Validator (Python version) | |
| Equivalent to the provided validate-submission.sh: | |
| 1) Ping HF Space /reset | |
| 2) Run docker build | |
| 3) Run openenv validate | |
| Usage: | |
| python validation.py <ping_url> [repo_dir] | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import urllib.error | |
| import urllib.request | |
| from dataclasses import dataclass | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| DOCKER_BUILD_TIMEOUT = 600 | |
| class Colors: | |
| red: str = "" | |
| green: str = "" | |
| yellow: str = "" | |
| bold: str = "" | |
| nc: str = "" | |
| def _colors() -> Colors: | |
| if sys.stdout.isatty(): | |
| return Colors( | |
| red="\033[0;31m", | |
| green="\033[0;32m", | |
| yellow="\033[1;33m", | |
| bold="\033[1m", | |
| nc="\033[0m", | |
| ) | |
| return Colors() | |
| C = _colors() | |
| def now_utc_hms() -> str: | |
| return datetime.now(timezone.utc).strftime("%H:%M:%S") | |
| def log(message: str) -> None: | |
| print(f"[{now_utc_hms()}] {message}") | |
| def pass_msg(message: str) -> None: | |
| log(f"{C.green}PASSED{C.nc} -- {message}") | |
| def fail_msg(message: str) -> None: | |
| log(f"{C.red}FAILED{C.nc} -- {message}") | |
| def hint(message: str) -> None: | |
| print(f" {C.yellow}Hint:{C.nc} {message}") | |
| def stop_at(step_name: str) -> None: | |
| print() | |
| print(f"{C.red}{C.bold}Validation stopped at {step_name}.{C.nc} Fix the above before continuing.") | |
| raise SystemExit(1) | |
| def run_command( | |
| cmd: list[str], | |
| *, | |
| cwd: Path | None = None, | |
| timeout: int | None = None, | |
| extra_env: dict[str, str] | None = None, | |
| ) -> tuple[int, str]: | |
| env = os.environ.copy() | |
| if extra_env: | |
| env.update(extra_env) | |
| try: | |
| proc = subprocess.run( | |
| cmd, | |
| cwd=str(cwd) if cwd else None, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| text=True, | |
| timeout=timeout, | |
| env=env, | |
| check=False, | |
| ) | |
| return proc.returncode, proc.stdout or "" | |
| except subprocess.TimeoutExpired as exc: | |
| output = (exc.stdout or "") + (exc.stderr or "") | |
| return 124, output | |
| def resolve_repo_dir(repo_dir_raw: str) -> Path: | |
| repo = Path(repo_dir_raw).expanduser().resolve() | |
| if not repo.exists() or not repo.is_dir(): | |
| print(f"Error: directory '{repo_dir_raw}' not found") | |
| raise SystemExit(1) | |
| return repo | |
| def normalize_ping_url(url: str) -> str: | |
| return url.rstrip("/") | |
| def check_step1_ping(ping_url: str) -> None: | |
| log(f"{C.bold}Step 1/3: Pinging HF Space{C.nc} ({ping_url}/reset) ...") | |
| payload = b"{}" | |
| req = urllib.request.Request( | |
| f"{ping_url}/reset", | |
| data=payload, | |
| method="POST", | |
| headers={"Content-Type": "application/json"}, | |
| ) | |
| try: | |
| with urllib.request.urlopen(req, timeout=30) as resp: | |
| code = resp.getcode() | |
| except urllib.error.HTTPError as exc: | |
| code = exc.code | |
| except Exception: | |
| code = 0 | |
| if code == 200: | |
| pass_msg("HF Space is live and responds to /reset") | |
| return | |
| if code == 0: | |
| fail_msg("HF Space not reachable (connection failed or timed out)") | |
| hint("Check your network connection and that the Space is running.") | |
| hint(f"Try: curl -s -o /dev/null -w '%{{http_code}}' -X POST {ping_url}/reset") | |
| stop_at("Step 1") | |
| fail_msg(f"HF Space /reset returned HTTP {code} (expected 200)") | |
| hint("Make sure your Space is running and the URL is correct.") | |
| hint(f"Try opening {ping_url} in your browser first.") | |
| stop_at("Step 1") | |
| def find_docker_context(repo_dir: Path) -> tuple[Path, Path] | None: | |
| root_docker = repo_dir / "Dockerfile" | |
| server_docker = repo_dir / "server" / "Dockerfile" | |
| nested_server_docker = repo_dir / "triage_env" / "server" / "Dockerfile" | |
| if root_docker.exists(): | |
| return repo_dir, root_docker | |
| if server_docker.exists(): | |
| return repo_dir, server_docker | |
| if nested_server_docker.exists(): | |
| return repo_dir, nested_server_docker | |
| return None | |
| def find_openenv_dir(repo_dir: Path) -> Path | None: | |
| """Find the directory containing the single source-of-truth openenv.yaml.""" | |
| if (repo_dir / "openenv.yaml").exists(): | |
| return repo_dir | |
| return None | |
| def find_openenv_executable(repo_dir: Path) -> str | None: | |
| """Resolve openenv executable from PATH or local virtualenv locations.""" | |
| in_path = shutil.which("openenv") | |
| if in_path: | |
| return in_path | |
| candidates = [ | |
| repo_dir / ".venv" / "Scripts" / "openenv.exe", | |
| repo_dir / ".venv" / "Scripts" / "openenv", | |
| repo_dir / ".venv" / "bin" / "openenv", | |
| ] | |
| for candidate in candidates: | |
| if candidate.exists(): | |
| return str(candidate) | |
| return None | |
| def check_step2_docker_build(repo_dir: Path) -> None: | |
| log(f"{C.bold}Step 2/3: Running docker build{C.nc} ...") | |
| if shutil.which("docker") is None: | |
| fail_msg("docker command not found") | |
| hint("Install Docker: https://docs.docker.com/get-docker/") | |
| stop_at("Step 2") | |
| docker_info = find_docker_context(repo_dir) | |
| if docker_info is None: | |
| fail_msg("No Dockerfile found in repo root, server/, or triage_env/server/") | |
| stop_at("Step 2") | |
| docker_context, dockerfile_path = docker_info | |
| log(f" Found Dockerfile: {dockerfile_path}") | |
| log(f" Build context: {docker_context}") | |
| rc, output = run_command( | |
| ["docker", "build", "-f", str(dockerfile_path), str(docker_context)], | |
| timeout=DOCKER_BUILD_TIMEOUT, | |
| ) | |
| if rc == 0: | |
| pass_msg("Docker build succeeded") | |
| return | |
| fail_msg(f"Docker build failed (timeout={DOCKER_BUILD_TIMEOUT}s)") | |
| tail = "\n".join((output or "").splitlines()[-20:]) | |
| if tail: | |
| print(tail) | |
| stop_at("Step 2") | |
| def check_step3_openenv_validate(repo_dir: Path) -> None: | |
| log(f"{C.bold}Step 3/3: Running openenv validate{C.nc} ...") | |
| openenv_executable = find_openenv_executable(repo_dir) | |
| if openenv_executable is None: | |
| fail_msg("openenv command not found") | |
| hint("Install it in your active environment: pip install openenv-core") | |
| hint("Or run validator via project venv Python: .venv/Scripts/python validation.py ...") | |
| stop_at("Step 3") | |
| # Find the actual OpenEnv environment directory | |
| env_dir = find_openenv_dir(repo_dir) | |
| if env_dir is None: | |
| fail_msg("openenv.yaml not found at repository root") | |
| hint(f"Make sure openenv.yaml is in {repo_dir}") | |
| stop_at("Step 3") | |
| log(f" Found openenv.yaml in: {env_dir}") | |
| rc, output = run_command([openenv_executable, "validate"], cwd=env_dir) | |
| if rc == 0: | |
| pass_msg("openenv validate passed") | |
| if output.strip(): | |
| log(f" {output.strip()}") | |
| return | |
| fail_msg("openenv validate failed") | |
| print(output) | |
| stop_at("Step 3") | |
| def print_header(repo_dir: Path, ping_url: str) -> None: | |
| print() | |
| print(f"{C.bold}========================================{C.nc}") | |
| print(f"{C.bold} OpenEnv Submission Validator{C.nc}") | |
| print(f"{C.bold}========================================{C.nc}") | |
| log(f"Repo: {repo_dir}") | |
| log(f"Ping URL: {ping_url}") | |
| print() | |
| def print_success_footer() -> None: | |
| print() | |
| print(f"{C.bold}========================================{C.nc}") | |
| print(f"{C.green}{C.bold} All 3/3 checks passed!{C.nc}") | |
| print(f"{C.green}{C.bold} Your submission is ready to submit.{C.nc}") | |
| print(f"{C.bold}========================================{C.nc}") | |
| print() | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="OpenEnv submission validator (Python equivalent of validate-submission.sh)" | |
| ) | |
| parser.add_argument("ping_url", help="Hugging Face Space URL, e.g. https://your-space.hf.space") | |
| parser.add_argument("repo_dir", nargs="?", default=".", help="Path to repository (default: current dir)") | |
| return parser.parse_args() | |
| def main() -> int: | |
| args = parse_args() | |
| ping_url = normalize_ping_url(args.ping_url) | |
| repo_dir = resolve_repo_dir(args.repo_dir) | |
| os.environ["PING_URL"] = ping_url | |
| print_header(repo_dir, ping_url) | |
| check_step1_ping(ping_url) | |
| check_step2_docker_build(repo_dir) | |
| check_step3_openenv_validate(repo_dir) | |
| print_success_footer() | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |