generated from coulomb/repo-seed
Some checks failed
ci / validate-registry (push) Has been cancelled
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.
163 lines
5.7 KiB
Python
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] |