from __future__ import annotations import json import sqlite3 from datetime import datetime, timezone from pathlib import Path from typing import Any import yaml from jsonschema import Draft202012Validator SCHEMA_PATH = Path(__file__).resolve().parent / "hub-registration.schema.yaml" def _utc_now() -> str: return datetime.now(timezone.utc).replace(microsecond=0).isoformat() def _load_schema() -> dict[str, Any]: return yaml.safe_load(SCHEMA_PATH.read_text(encoding="utf-8")) def _validate(payload: dict[str, Any], schema_ref: str) -> list[str]: schema = _load_schema() subschema = schema["$defs"][schema_ref] validator = Draft202012Validator(subschema) return [error.message for error in validator.iter_errors(payload)] def _row_to_registration(row: sqlite3.Row) -> dict[str, Any]: data = json.loads(row["payload"]) data["registered_at"] = row["registered_at"] data["updated_at"] = row["updated_at"] return data def _public_registration(registration: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in registration.items() if key != "auth_env"} class HubStore: def __init__(self, db_path: Path) -> None: self.db_path = db_path self.db_path.parent.mkdir(parents=True, exist_ok=True) self._init_db() def _connect(self) -> sqlite3.Connection: conn = sqlite3.connect(self.db_path) conn.row_factory = sqlite3.Row return conn def _init_db(self) -> None: with self._connect() as conn: conn.execute( """ CREATE TABLE IF NOT EXISTS registrations ( repo TEXT PRIMARY KEY, payload TEXT NOT NULL, registered_at TEXT NOT NULL, updated_at TEXT NOT NULL ) """ ) def list_repos(self) -> list[dict[str, Any]]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM registrations ORDER BY repo" ).fetchall() return [_public_registration(_row_to_registration(row)) for row in rows] def get_repo(self, repo: str) -> dict[str, Any] | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM registrations WHERE repo = ?", (repo,) ).fetchone() if row is None: return None return _public_registration(_row_to_registration(row)) def get_repo_internal(self, repo: str) -> dict[str, Any] | None: with self._connect() as conn: row = conn.execute( "SELECT * FROM registrations WHERE repo = ?", (repo,) ).fetchone() if row is None: return None return _row_to_registration(row) def create_repo(self, payload: dict[str, Any]) -> dict[str, Any]: errors = _validate(payload, "registration_request") if errors: raise ValueError("; ".join(errors)) now = _utc_now() record: dict[str, Any] = { "repo": payload["repo"], "url": payload["url"], "enabled": payload.get("enabled", True), "required": payload.get("required", False), "domain": payload["domain"], "cache_ttl_seconds": payload.get("cache_ttl_seconds", 86400), "auth_header": payload.get("auth_header", "Authorization"), } for optional in ("description", "auth_env", "registered_by"): if payload.get(optional) is not None: record[optional] = payload[optional] validator = Draft202012Validator(_load_schema()) full_errors = [ error.message for error in validator.iter_errors( {**record, "registered_at": now, "updated_at": now} ) ] if full_errors: raise ValueError("; ".join(full_errors)) with self._connect() as conn: try: conn.execute( """ INSERT INTO registrations (repo, payload, registered_at, updated_at) VALUES (?, ?, ?, ?) """, (record["repo"], json.dumps(record), now, now), ) except sqlite3.IntegrityError as exc: raise FileExistsError(f"repo already registered: {record['repo']}") from exc return _public_registration({**record, "registered_at": now, "updated_at": now}) def update_repo(self, repo: str, payload: dict[str, Any]) -> dict[str, Any]: errors = _validate(payload, "registration_update") if errors: raise ValueError("; ".join(errors)) existing = self.get_repo_internal(repo) if existing is None: raise KeyError(f"repo not found: {repo}") updated = {**existing, **{k: v for k, v in payload.items() if v is not None}, "repo": repo} now = _utc_now() updated["updated_at"] = now with self._connect() as conn: conn.execute( """ UPDATE registrations SET payload = ?, updated_at = ? WHERE repo = ? """, (json.dumps(updated), now, repo), ) return _public_registration(updated) def delete_repo(self, repo: str) -> bool: with self._connect() as conn: cursor = conn.execute( "DELETE FROM registrations WHERE repo = ?", (repo,) ) return cursor.rowcount > 0 def list_repos_internal(self) -> list[dict[str, Any]]: with self._connect() as conn: rows = conn.execute( "SELECT * FROM registrations ORDER BY repo" ).fetchall() return [_row_to_registration(row) for row in rows]