generated from coulomb/repo-seed
Harden registry API and schema validation
This commit is contained in:
@@ -7,9 +7,8 @@ from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import jsonschema
|
||||
|
||||
from .loader import load_yaml, repo_root
|
||||
from .loader import repo_root
|
||||
from .schema_validation import draft202012_validator
|
||||
|
||||
|
||||
class RegistryError(Exception):
|
||||
@@ -530,6 +529,30 @@ class RegistryStore:
|
||||
],
|
||||
}
|
||||
|
||||
def status(self) -> dict[str, Any]:
|
||||
with self._connect() as db:
|
||||
counts = {
|
||||
"repositories": db.execute("select count(*) from repositories").fetchone()[0],
|
||||
"snapshots": db.execute("select count(*) from snapshots").fetchone()[0],
|
||||
"artifacts": db.execute("select count(*) from artifacts").fetchone()[0],
|
||||
"libraries": db.execute("select count(*) from libraries").fetchone()[0],
|
||||
}
|
||||
latest = [
|
||||
{
|
||||
"repo_slug": snapshot["repo_slug"],
|
||||
"snapshot_id": snapshot["id"],
|
||||
"commit": snapshot["commit"],
|
||||
"generated_at": snapshot["generated_at"],
|
||||
}
|
||||
for snapshot in self.latest_snapshots()
|
||||
]
|
||||
return {
|
||||
"status": "ok",
|
||||
"database": str(self.path),
|
||||
"counts": counts,
|
||||
"latest_snapshots": latest,
|
||||
}
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
db = sqlite3.connect(self.path)
|
||||
db.row_factory = sqlite3.Row
|
||||
@@ -537,19 +560,8 @@ class RegistryStore:
|
||||
|
||||
|
||||
def validate_graph_export(graph: dict[str, Any]) -> None:
|
||||
schemas_dir = repo_root() / "schemas"
|
||||
schema_path = schemas_dir / "state-hub-export.schema.yaml"
|
||||
store = {
|
||||
path.resolve().as_uri(): load_yaml(path)
|
||||
for path in sorted(schemas_dir.glob("*.schema.yaml"))
|
||||
}
|
||||
schema = load_yaml(schema_path)
|
||||
resolver = jsonschema.RefResolver(
|
||||
base_uri=schema_path.resolve().as_uri(),
|
||||
referrer=schema,
|
||||
store=store,
|
||||
)
|
||||
validator = jsonschema.Draft202012Validator(schema, resolver=resolver)
|
||||
schema_path = repo_root() / "schemas" / "state-hub-export.schema.yaml"
|
||||
validator = draft202012_validator(schema_path)
|
||||
errors = sorted(validator.iter_errors(graph), key=lambda error: list(error.path))
|
||||
if errors:
|
||||
error = errors[0]
|
||||
|
||||
67
railiance_fabric/schema_validation.py
Normal file
67
railiance_fabric/schema_validation.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import jsonschema
|
||||
|
||||
try:
|
||||
from referencing import Registry, Resource
|
||||
except ModuleNotFoundError:
|
||||
Registry = None
|
||||
Resource = None
|
||||
|
||||
from .loader import load_yaml
|
||||
|
||||
|
||||
def schema_registry(schemas_dir: Path) -> Any:
|
||||
if Registry is None or Resource is None:
|
||||
return None
|
||||
|
||||
resources: list[tuple[str, Any]] = []
|
||||
for path in sorted(schemas_dir.glob("*.schema.yaml")):
|
||||
schema = load_yaml(path)
|
||||
if not isinstance(schema, dict):
|
||||
continue
|
||||
resource = Resource.from_contents(schema)
|
||||
resources.append((path.resolve().as_uri(), resource))
|
||||
schema_id = schema.get("$id")
|
||||
if isinstance(schema_id, str):
|
||||
resources.append((schema_id, resource))
|
||||
return Registry().with_resources(resources)
|
||||
|
||||
|
||||
def draft202012_validator(schema_path: Path) -> jsonschema.Draft202012Validator:
|
||||
schema = load_yaml(schema_path)
|
||||
if not isinstance(schema, dict):
|
||||
raise ValueError(f"schema must be a mapping: {schema_path}")
|
||||
registry = schema_registry(schema_path.parent)
|
||||
if registry is None:
|
||||
return _legacy_validator(schema_path, schema)
|
||||
return jsonschema.Draft202012Validator(
|
||||
schema,
|
||||
registry=registry,
|
||||
)
|
||||
|
||||
|
||||
def _legacy_validator(
|
||||
schema_path: Path,
|
||||
schema: dict[str, Any],
|
||||
) -> jsonschema.Draft202012Validator:
|
||||
store: dict[str, Any] = {}
|
||||
for path in sorted(schema_path.parent.glob("*.schema.yaml")):
|
||||
loaded_schema = load_yaml(path)
|
||||
store[path.resolve().as_uri()] = loaded_schema
|
||||
if isinstance(loaded_schema, dict):
|
||||
schema_id = loaded_schema.get("$id")
|
||||
if isinstance(schema_id, str):
|
||||
store[schema_id] = loaded_schema
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
resolver = jsonschema.RefResolver(
|
||||
base_uri=schema_path.resolve().as_uri(),
|
||||
referrer=schema,
|
||||
store=store,
|
||||
)
|
||||
return jsonschema.Draft202012Validator(schema, resolver=resolver)
|
||||
@@ -39,6 +39,8 @@ class RegistryHandler(BaseHTTPRequestHandler):
|
||||
parts = _parts(path)
|
||||
if path == "/health":
|
||||
return HTTPStatus.OK, {"status": "ok"}
|
||||
if path == "/status":
|
||||
return HTTPStatus.OK, self.store.status()
|
||||
if parts == ["repositories"]:
|
||||
return HTTPStatus.OK, self.store.list_repositories()
|
||||
if len(parts) == 3 and parts[0] == "repositories" and parts[2] == "inventory":
|
||||
|
||||
@@ -4,10 +4,9 @@ from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import jsonschema
|
||||
|
||||
from .loader import load_declarations, load_yaml, repo_root
|
||||
from .model import Declaration, ValidationReport
|
||||
from .schema_validation import draft202012_validator
|
||||
|
||||
SCHEMA_BY_KIND = {
|
||||
"ServiceDeclaration": "service.schema.yaml",
|
||||
@@ -32,10 +31,6 @@ def validate_roots(paths: list[Path]) -> ValidationReport:
|
||||
|
||||
def _validate_schema(root: Path, declarations: list[Declaration], report: ValidationReport) -> None:
|
||||
schemas_dir = root / "schemas"
|
||||
store = {
|
||||
path.resolve().as_uri(): load_yaml(path)
|
||||
for path in sorted(schemas_dir.glob("*.schema.yaml"))
|
||||
}
|
||||
|
||||
for declaration in declarations:
|
||||
schema_name = SCHEMA_BY_KIND.get(declaration.kind)
|
||||
@@ -49,13 +44,7 @@ def _validate_schema(root: Path, declarations: list[Declaration], report: Valida
|
||||
continue
|
||||
|
||||
schema_path = schemas_dir / schema_name
|
||||
schema = load_yaml(schema_path)
|
||||
resolver = jsonschema.RefResolver(
|
||||
base_uri=schema_path.resolve().as_uri(),
|
||||
referrer=schema,
|
||||
store=store,
|
||||
)
|
||||
validator = jsonschema.Draft202012Validator(schema, resolver=resolver)
|
||||
validator = draft202012_validator(schema_path)
|
||||
for error in sorted(validator.iter_errors(declaration.data), key=lambda e: list(e.path)):
|
||||
location = ".".join(str(part) for part in error.path) or "<root>"
|
||||
report.add(
|
||||
|
||||
Reference in New Issue
Block a user