dataforge-playground / dataforge /release /playground_check.py
Praneshrajan15's picture
Deploy DataForge playground API
13fe504 verified
Raw
History Blame Contribute Delete
14.5 kB
"""Live Playground release and monitoring checks."""
from __future__ import annotations
import json
import time
from dataclasses import asdict, dataclass
from typing import Any
from urllib.parse import urlsplit
import httpx
from dataforge.release.doctor import run_doctor
DEFAULT_BACKEND_URL = "https://Praneshrajan15-dataforge-playground.hf.space"
DEFAULT_FRONTEND_URL = "https://dataforge.praneshrajan15.workers.dev/playground"
NEGATIVE_CORS_ORIGIN = "https://untrusted-dataforge.example"
REQUIRED_HEALTH_KEYS = {"status", "advanced_available", "max_upload_bytes"}
ENHANCED_HEALTH_KEYS = {
"service",
"api_version",
"contract_version",
"build_sha",
"server_time_utc",
"environment",
"limits",
"cors_configured",
"otel_enabled",
"metrics",
}
@dataclass(frozen=True)
class PlaygroundCheck:
"""One Playground check result."""
name: str
ok: bool
detail: str
metadata: dict[str, Any]
@dataclass(frozen=True)
class PlaygroundCheckReport:
"""Machine-readable Playground release report."""
ok: bool
frontend_url: str
backend_url: str
checks: list[PlaygroundCheck]
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-serializable report."""
return asdict(self)
def normalize_url(value: str) -> str:
"""Strip whitespace and trailing slashes from a URL."""
return value.strip().rstrip("/")
def frontend_origin(frontend_url: str) -> str:
"""Return the origin portion of a frontend URL."""
parts = urlsplit(frontend_url)
return f"{parts.scheme}://{parts.netloc}"
def join_url(base_url: str, path: str) -> str:
"""Join a normalized base URL and absolute path fragment."""
return f"{base_url.rstrip('/')}/{path.lstrip('/')}"
def _timed_request(
client: httpx.Client, method: str, url: str, **kwargs: Any
) -> tuple[httpx.Response, float]:
"""Run one HTTP request and return response plus elapsed milliseconds."""
started = time.perf_counter()
response = client.request(method, url, **kwargs)
return response, (time.perf_counter() - started) * 1000
def _check_frontend_deployed(
client: httpx.Client,
*,
frontend_url: str,
) -> PlaygroundCheck:
try:
response, latency_ms = _timed_request(client, "GET", frontend_url)
body = response.text
ok = (
response.status_code == 200
and "<!doctype html>" in body.lower()
and 'id="root"' in body
and "config.js" in body
and "/playground/assets/" in body
)
detail = (
"Frontend serves the React shell."
if ok
else "Frontend shell is missing required markers."
)
return PlaygroundCheck(
"frontend_deployed",
ok,
detail,
{
"status_code": response.status_code,
"latency_ms": round(latency_ms, 2),
"content_type": response.headers.get("content-type", ""),
},
)
except Exception as exc:
return PlaygroundCheck("frontend_deployed", False, str(exc), {})
def _check_config_js(
client: httpx.Client,
*,
frontend_url: str,
backend_url: str,
) -> PlaygroundCheck:
try:
response, latency_ms = _timed_request(client, "GET", join_url(frontend_url, "config.js"))
cache_control = response.headers.get("cache-control", "")
ok = (
response.status_code == 200
and backend_url in response.text
and "no-store" in cache_control.lower()
)
detail = (
"config.js points at the expected backend and is uncached."
if ok
else "config.js is stale or cacheable."
)
return PlaygroundCheck(
"config_js_correct",
ok,
detail,
{
"status_code": response.status_code,
"latency_ms": round(latency_ms, 2),
"cache_control": cache_control,
},
)
except Exception as exc:
return PlaygroundCheck("config_js_correct", False, str(exc), {})
def _check_backend_deployed(
client: httpx.Client,
*,
backend_url: str,
latency_threshold_ms: float,
) -> PlaygroundCheck:
try:
root, root_latency_ms = _timed_request(client, "GET", backend_url)
health, health_latency_ms = _timed_request(client, "GET", f"{backend_url}/api/health")
payload = health.json() if health.status_code == 200 else {}
missing = sorted(REQUIRED_HEALTH_KEYS - set(payload))
enhanced_missing = sorted(ENHANCED_HEALTH_KEYS - set(payload))
ok = (
root.status_code == 200
and health.status_code == 200
and payload.get("status") == "ok"
and not missing
and health_latency_ms <= latency_threshold_ms
)
detail = (
"Backend root and health are reachable."
if ok
else "Backend root or health check failed."
)
return PlaygroundCheck(
"backend_deployed",
ok,
detail,
{
"root_status_code": root.status_code,
"health_status_code": health.status_code,
"root_latency_ms": round(root_latency_ms, 2),
"health_latency_ms": round(health_latency_ms, 2),
"latency_threshold_ms": latency_threshold_ms,
"required_missing": missing,
"enhanced_missing": enhanced_missing,
"metrics": payload.get("metrics", {}),
},
)
except Exception as exc:
return PlaygroundCheck("backend_deployed", False, str(exc), {})
def _check_cors(
client: httpx.Client,
*,
frontend_url: str,
backend_url: str,
) -> PlaygroundCheck:
origin = frontend_origin(frontend_url)
try:
positive, positive_latency_ms = _timed_request(
client,
"GET",
f"{backend_url}/api/health",
headers={"Origin": origin},
)
negative, negative_latency_ms = _timed_request(
client,
"GET",
f"{backend_url}/api/health",
headers={"Origin": NEGATIVE_CORS_ORIGIN},
)
preflight, preflight_latency_ms = _timed_request(
client,
"OPTIONS",
f"{backend_url}/api/health",
headers={
"Origin": NEGATIVE_CORS_ORIGIN,
"Access-Control-Request-Method": "GET",
},
)
allowed_origin = positive.headers.get("access-control-allow-origin")
negative_allowed_origin = negative.headers.get("access-control-allow-origin")
preflight_allowed_origin = preflight.headers.get("access-control-allow-origin")
negative_error = ""
try:
negative_error = str(negative.json().get("error", ""))
except ValueError:
negative_error = ""
negative_denied = negative.status_code == 403 and negative_error == "origin_not_allowed"
ok = allowed_origin == origin and positive.status_code == 200 and negative_denied
detail = (
"Configured origin is allowed and disallowed origins cannot read API data."
if ok
else "CORS origin enforcement is incorrect."
)
return PlaygroundCheck(
"cors_correct",
ok,
detail,
{
"frontend_origin": origin,
"positive_status_code": positive.status_code,
"negative_status_code": negative.status_code,
"allowed_origin": allowed_origin,
"negative_allowed_origin": negative_allowed_origin,
"negative_error": negative_error,
"negative_preflight_status_code": preflight.status_code,
"negative_preflight_allowed_origin": preflight_allowed_origin,
"positive_latency_ms": round(positive_latency_ms, 2),
"negative_latency_ms": round(negative_latency_ms, 2),
"negative_preflight_latency_ms": round(preflight_latency_ms, 2),
},
)
except Exception as exc:
return PlaygroundCheck("cors_correct", False, str(exc), {})
def _check_doctor() -> PlaygroundCheck:
report = run_doctor(core=True, maintainer_deploy=False)
return PlaygroundCheck(
"doctor_passing",
report.ok,
"Core release doctor passed." if report.ok else "Core release doctor failed.",
{"doctor": report.to_dict()},
)
def _check_smoke_flow(client: httpx.Client, *, backend_url: str) -> PlaygroundCheck:
try:
sample, sample_latency_ms = _timed_request(
client,
"GET",
f"{backend_url}/api/samples/hospital_10rows",
)
if sample.status_code != 200:
return PlaygroundCheck(
"smoke_flow_passing",
False,
"Sample endpoint failed.",
{"sample_status_code": sample.status_code},
)
files = {"file": ("hospital_10rows.csv", sample.content, "text/csv")}
analyze, analyze_latency_ms = _timed_request(
client,
"POST",
f"{backend_url}/api/analyze",
files=files,
)
files = {"file": ("hospital_10rows.csv", sample.content, "text/csv")}
profile, profile_latency_ms = _timed_request(
client,
"POST",
f"{backend_url}/api/profile",
files=files,
)
files = {"file": ("hospital_10rows.csv", sample.content, "text/csv")}
repair, repair_latency_ms = _timed_request(
client,
"POST",
f"{backend_url}/api/repair",
files=files,
)
analyze_payload = analyze.json() if analyze.status_code == 200 else {}
profile_payload = profile.json() if profile.status_code == 200 else {}
repair_payload = repair.json() if repair.status_code == 200 else {}
analyze_required_keys = {
"source",
"risk_summary",
"repairs",
"verification",
"receipt",
"apply_handoff",
}
analyze_missing = sorted(analyze_required_keys - set(analyze_payload))
ok = (
analyze.status_code == 200
and profile.status_code == 200
and repair.status_code == 200
and not analyze_missing
and "issues" in profile_payload
and "fixes" in repair_payload
and "txn_journal" in repair_payload
)
return PlaygroundCheck(
"smoke_flow_passing",
ok,
(
"Sample analyze, profile, and repair dry-run passed."
if ok
else "Sample smoke flow failed."
),
{
"sample_latency_ms": round(sample_latency_ms, 2),
"analyze_status_code": analyze.status_code,
"profile_status_code": profile.status_code,
"repair_status_code": repair.status_code,
"analyze_latency_ms": round(analyze_latency_ms, 2),
"profile_latency_ms": round(profile_latency_ms, 2),
"repair_latency_ms": round(repair_latency_ms, 2),
"analyze_missing": analyze_missing,
"issue_count": len(profile_payload.get("issues", []))
if isinstance(profile_payload, dict)
else None,
"repair_count": len(analyze_payload.get("repairs", []))
if isinstance(analyze_payload, dict)
else None,
"fix_count": len(repair_payload.get("fixes", []))
if isinstance(repair_payload, dict)
else None,
},
)
except Exception as exc:
return PlaygroundCheck("smoke_flow_passing", False, str(exc), {})
def run_playground_check(
*,
frontend_url: str = DEFAULT_FRONTEND_URL,
backend_url: str = DEFAULT_BACKEND_URL,
latency_threshold_ms: float = 5_000.0,
include_doctor: bool = True,
include_smoke: bool = True,
client: httpx.Client | None = None,
) -> PlaygroundCheckReport:
"""Run the public Playground release checklist."""
normalized_frontend_url = normalize_url(frontend_url)
normalized_backend_url = normalize_url(backend_url)
def collect(active_client: httpx.Client) -> list[PlaygroundCheck]:
checks = [
_check_frontend_deployed(active_client, frontend_url=normalized_frontend_url),
_check_config_js(
active_client,
frontend_url=normalized_frontend_url,
backend_url=normalized_backend_url,
),
_check_backend_deployed(
active_client,
backend_url=normalized_backend_url,
latency_threshold_ms=latency_threshold_ms,
),
_check_cors(
active_client,
frontend_url=normalized_frontend_url,
backend_url=normalized_backend_url,
),
]
if include_doctor:
checks.append(_check_doctor())
if include_smoke:
checks.append(_check_smoke_flow(active_client, backend_url=normalized_backend_url))
return checks
if client is not None:
checks = collect(client)
else:
with httpx.Client(follow_redirects=True, timeout=30.0) as owned_client:
checks = collect(owned_client)
return PlaygroundCheckReport(
ok=all(check.ok for check in checks),
frontend_url=normalized_frontend_url,
backend_url=normalized_backend_url,
checks=checks,
)
def report_to_json(report: PlaygroundCheckReport) -> str:
"""Render a stable JSON report."""
return json.dumps(report.to_dict(), indent=2, sort_keys=True)