Skip to content

Commit 1ecdf09

Browse files
rustyconoverclaude
andcommitted
feat(landing): standardized worker landing page + describe.json contract
Replace the bespoke per-worker HTML page with the shared, self-contained landing.html (vendored byte-identical across all VGI languages) served at GET / with content negotiation. It fetches a small, versioned describe.json contract that this repo produces from live catalog introspection. - vgi/http/describe_json.py: build describe.json (catalogs, tags, attach options, data versions, per-schema tables/views/functions with named+ positional args) + the lazy per-object column endpoint. - vgi/http/landing_page.py: Falcon resources for GET /, /describe.json, and /describe/{catalog}/{schema}/{table}.json. - vgi/http/landing.html: vendored shared page (VGI logo header linking to query.farm, Cupola CTA, DuckDB/Python/Node connect examples). - serve.py + fixture http_server: route the standardized surface; drop the server-side user-info injection (the page reads _vgi_identity itself). - tests/test_landing_conformance.py + tests/landing/: schema + golden + page marker + column-endpoint conformance guard. - Require vgi-rpc>=0.23.0 (identity cookie); bump 0.10.0 -> 0.11.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 708ee01 commit 1ecdf09

12 files changed

Lines changed: 4208 additions & 25 deletions

‎pyproject.toml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "vgi-python"
3-
version = "0.10.0"
3+
version = "0.11.0"
44
description = "Vector Gateway Interface - Connect DuckDB to external programs via Apache Arrow"
55
readme = "README.md"
66
keywords = [
@@ -40,7 +40,7 @@ dependencies = [
4040
"pyarrow",
4141
"typer>=0.9",
4242
"platformdirs",
43-
"vgi-rpc>=0.21.0",
43+
"vgi-rpc>=0.23.0",
4444
"httpx>=0.24",
4545
]
4646

@@ -82,6 +82,7 @@ dev = [
8282
# (a vgi-rpc dependency) in the shared `docstring_parser` namespace,
8383
# non-deterministically breaking imports. tests/test_docstrings.py runs it
8484
# in an isolated env via `uvx pydoclint` instead.
85+
"jsonschema",
8586
"pytest",
8687
"pytest-cov",
8788
"pytest-examples",

‎tests/landing/_checker.py‎

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python3
2+
"""Landing-page conformance checker for VGI workers (all languages).
3+
4+
Drives a running worker's HTTP landing surface and asserts it conforms to the
5+
contract in ``docs/http-landing-contract.md``:
6+
7+
* ``GET {prefix}/describe.json`` validates against ``describe.schema.json`` and
8+
(optionally) equals a **normalized golden** — the cross-language equality guard.
9+
* ``GET {prefix}/`` serves the pinned shared ``landing.html`` (asset marker), and
10+
``?format=json`` returns a JSON status.
11+
* ``GET {prefix}/describe/{catalog}/{schema}/{table}.json`` returns valid columns.
12+
13+
Usage:
14+
run_landing_conformance.py --url http://localhost:8790 [--golden fixtures/describe.expected.json]
15+
run_landing_conformance.py --url http://localhost:8790 --golden fixtures/describe.expected.json --update
16+
17+
Exit code is non-zero on any failure. ``--update`` (re)writes the golden from the
18+
live worker (reviewers inspect the diff).
19+
"""
20+
21+
from __future__ import annotations
22+
23+
import argparse
24+
import json
25+
import pathlib
26+
import sys
27+
import urllib.request
28+
29+
import jsonschema
30+
31+
_HERE = pathlib.Path(__file__).resolve().parent
32+
DEFAULT_SCHEMA = _HERE / "describe.schema.json"
33+
ASSET_MARKER = "vgi-landing-asset v"
34+
35+
# Fields dropped before comparing to a golden (volatile or language-specific).
36+
_VOLATILE_TOP = ("server_id", "worker", "oauth")
37+
38+
39+
def _get(url: str, accept: str = "application/json") -> tuple[int, bytes, str]:
40+
req = urllib.request.Request(url, headers={"Accept": accept})
41+
try:
42+
with urllib.request.urlopen(req) as resp: # noqa: S310 — trusted local URL
43+
return resp.status, resp.read(), resp.headers.get("Content-Type", "")
44+
except urllib.error.HTTPError as exc: # type: ignore[attr-defined]
45+
return exc.code, exc.read(), exc.headers.get("Content-Type", "")
46+
47+
48+
def normalize(doc: dict) -> dict:
49+
"""Strip volatile/language-specific fields so goldens compare across languages."""
50+
return {k: v for k, v in doc.items() if k not in _VOLATILE_TOP}
51+
52+
53+
def _canonical(doc: dict) -> str:
54+
return json.dumps(doc, indent=2, sort_keys=True, ensure_ascii=False)
55+
56+
57+
def check(
58+
base_url: str,
59+
*,
60+
schema_path: pathlib.Path = DEFAULT_SCHEMA,
61+
golden_path: pathlib.Path | None = None,
62+
) -> list[str]:
63+
"""Return a list of failure strings (empty == conformant)."""
64+
base = base_url.rstrip("/")
65+
fails: list[str] = []
66+
schema = json.loads(schema_path.read_text())
67+
columns_schema = {**schema, **schema["$defs"]["columns"]}
68+
69+
# 1) describe.json — schema-valid.
70+
status, body, _ = _get(f"{base}/describe.json")
71+
if status != 200:
72+
return [f"GET /describe.json returned HTTP {status}"]
73+
doc = json.loads(body)
74+
try:
75+
jsonschema.validate(doc, schema)
76+
except jsonschema.ValidationError as exc:
77+
fails.append(f"describe.json schema violation: {exc.message} at {list(exc.absolute_path)}")
78+
79+
# 2) GET / serves the pinned shared page; ?format=json is a JSON status.
80+
status, html, ctype = _get(f"{base}/", accept="text/html")
81+
if status != 200 or "text/html" not in ctype:
82+
fails.append(f"GET / (Accept: text/html) returned HTTP {status} {ctype!r}")
83+
elif ASSET_MARKER not in html.decode("utf-8", "replace"):
84+
fails.append(f"GET / did not serve the pinned landing.html (missing {ASSET_MARKER!r} marker)")
85+
status, jbody, _ = _get(f"{base}/?format=json")
86+
try:
87+
assert json.loads(jbody).get("status") == "ok"
88+
except Exception: # noqa: BLE001
89+
fails.append("GET /?format=json did not return a JSON status object")
90+
91+
# 3) Lazy column endpoints — validate one table + one view per schema.
92+
for cat in doc.get("catalogs", []):
93+
for sch in cat.get("schemas", []):
94+
samples = (sch.get("tables") or [])[:1] + (sch.get("views") or [])[:1]
95+
for obj in samples:
96+
cstatus, cbody, _ = _get(f"{base}/describe/{cat['name']}/{sch['name']}/{obj['name']}.json")
97+
if cstatus != 200:
98+
fails.append(f"columns {cat['name']}/{sch['name']}/{obj['name']} -> HTTP {cstatus}")
99+
continue
100+
try:
101+
jsonschema.validate(json.loads(cbody), columns_schema)
102+
except jsonschema.ValidationError as exc:
103+
fails.append(f"columns {sch['name']}.{obj['name']} schema violation: {exc.message}")
104+
105+
# 4) Golden diff (cross-language equality).
106+
if golden_path is not None:
107+
got = _canonical(normalize(doc))
108+
want = golden_path.read_text() if golden_path.exists() else None
109+
if want is None:
110+
fails.append(f"golden {golden_path} does not exist (run with --update)")
111+
elif got.strip() != want.strip():
112+
fails.append(f"describe.json does not match golden {golden_path.name} (normalized diff)")
113+
114+
return fails
115+
116+
117+
def main() -> int:
118+
ap = argparse.ArgumentParser(description=__doc__)
119+
ap.add_argument("--url", required=True, help="Worker base URL, e.g. http://localhost:8790")
120+
ap.add_argument("--schema", type=pathlib.Path, default=DEFAULT_SCHEMA)
121+
ap.add_argument("--golden", type=pathlib.Path, default=None)
122+
ap.add_argument("--update", action="store_true", help="Write the normalized golden from the live worker")
123+
args = ap.parse_args()
124+
125+
if args.update:
126+
if args.golden is None:
127+
ap.error("--update requires --golden")
128+
_, body, _ = _get(f"{args.url.rstrip('/')}/describe.json")
129+
args.golden.parent.mkdir(parents=True, exist_ok=True)
130+
args.golden.write_text(_canonical(normalize(json.loads(body))) + "\n")
131+
print(f"wrote golden {args.golden}")
132+
return 0
133+
134+
fails = check(args.url, schema_path=args.schema, golden_path=args.golden)
135+
if fails:
136+
print(f"FAIL ({len(fails)}):")
137+
for f in fails:
138+
print(f" - {f}")
139+
return 1
140+
print("PASS: landing conformance OK")
141+
return 0
142+
143+
144+
if __name__ == "__main__":
145+
sys.exit(main())

0 commit comments

Comments
 (0)