Files
reuse-surface/reuse_surface/hub/store.py
tegwick cb7a6e4f2e
Some checks failed
ci / validate-registry (push) Has been cancelled
Bundle hub registration schema in the installed package
Load the schema from reuse_surface/hub/ next to store.py and declare
package-data so pip install includes the YAML in site-packages. Fixes
hub register 500 in the container image.
2026-06-15 10:11:41 +02:00

163 lines
5.7 KiB
Python

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]