161 lines
5.9 KiB
Python
161 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from jsonschema import ValidationError, validate
|
|
from packaging.version import InvalidVersion, Version
|
|
|
|
from .models import AdapterContract
|
|
|
|
|
|
class GraphOfContractsRegistry:
|
|
def __init__(self, path: str | Path = ":memory:") -> None:
|
|
self.path = str(path)
|
|
self._conn = sqlite3.connect(self.path)
|
|
self._conn.row_factory = sqlite3.Row
|
|
self._ensure_schema()
|
|
|
|
def _ensure_schema(self) -> None:
|
|
self._conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS contracts (
|
|
name TEXT NOT NULL,
|
|
version TEXT NOT NULL,
|
|
adapter_type TEXT NOT NULL,
|
|
input_schema TEXT NOT NULL,
|
|
output_schema TEXT NOT NULL,
|
|
capabilities TEXT NOT NULL,
|
|
metadata TEXT NOT NULL,
|
|
fingerprint TEXT NOT NULL,
|
|
PRIMARY KEY (name, version)
|
|
)
|
|
"""
|
|
)
|
|
self._conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS edges (
|
|
source_name TEXT NOT NULL,
|
|
source_version TEXT NOT NULL,
|
|
target_name TEXT NOT NULL,
|
|
target_version TEXT NOT NULL,
|
|
relation TEXT NOT NULL,
|
|
PRIMARY KEY (source_name, source_version, target_name, target_version, relation)
|
|
)
|
|
"""
|
|
)
|
|
self._conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS conformance_checks (
|
|
contract_name TEXT NOT NULL,
|
|
contract_version TEXT NOT NULL,
|
|
payload_kind TEXT NOT NULL,
|
|
passed INTEGER NOT NULL,
|
|
error TEXT,
|
|
payload TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
self._conn.commit()
|
|
|
|
def register(self, contract: AdapterContract) -> AdapterContract:
|
|
self._conn.execute(
|
|
"""
|
|
INSERT OR REPLACE INTO contracts
|
|
(name, version, adapter_type, input_schema, output_schema, capabilities, metadata, fingerprint)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
contract.name,
|
|
contract.version,
|
|
contract.adapter_type,
|
|
json.dumps(contract.input_schema, sort_keys=True),
|
|
json.dumps(contract.output_schema, sort_keys=True),
|
|
json.dumps(contract.capabilities, sort_keys=True),
|
|
json.dumps(contract.metadata, sort_keys=True),
|
|
contract.fingerprint,
|
|
),
|
|
)
|
|
self._conn.commit()
|
|
return contract
|
|
|
|
def link(self, source: tuple[str, str], target: tuple[str, str], relation: str = "compatible_with") -> None:
|
|
self._conn.execute(
|
|
"INSERT OR REPLACE INTO edges VALUES (?, ?, ?, ?, ?)",
|
|
(source[0], source[1], target[0], target[1], relation),
|
|
)
|
|
self._conn.commit()
|
|
|
|
def latest(self, name: str) -> AdapterContract:
|
|
versions = self.versions(name)
|
|
if not versions:
|
|
raise KeyError(name)
|
|
latest_version = versions[-1]
|
|
return self.get(name, latest_version)
|
|
|
|
def get(self, name: str, version: str) -> AdapterContract:
|
|
row = self._conn.execute(
|
|
"SELECT * FROM contracts WHERE name = ? AND version = ?",
|
|
(name, version),
|
|
).fetchone()
|
|
if row is None:
|
|
raise KeyError(f"{name}@{version}")
|
|
return self._row_to_contract(row)
|
|
|
|
def versions(self, name: str) -> list[str]:
|
|
rows = self._conn.execute("SELECT version FROM contracts WHERE name = ? ORDER BY version", (name,)).fetchall()
|
|
versions = [row["version"] for row in rows]
|
|
try:
|
|
return sorted(versions, key=Version)
|
|
except InvalidVersion:
|
|
return sorted(versions)
|
|
|
|
def validate_payload(self, name: str, version: str, payload: dict[str, Any], *, kind: str = "input") -> None:
|
|
contract = self.get(name, version)
|
|
schema = contract.input_schema if kind == "input" else contract.output_schema
|
|
try:
|
|
validate(instance=payload, schema=schema)
|
|
passed = True
|
|
error = None
|
|
except ValidationError as exc:
|
|
passed = False
|
|
error = str(exc)
|
|
self._record_conformance(contract, kind, payload, passed, error)
|
|
raise
|
|
self._record_conformance(contract, kind, payload, passed, error)
|
|
|
|
def _record_conformance(self, contract: AdapterContract, kind: str, payload: dict[str, Any], passed: bool, error: str | None) -> None:
|
|
self._conn.execute(
|
|
"""
|
|
INSERT INTO conformance_checks
|
|
(contract_name, contract_version, payload_kind, passed, error, payload)
|
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(contract.name, contract.version, kind, int(passed), error, json.dumps(payload, sort_keys=True)),
|
|
)
|
|
self._conn.commit()
|
|
|
|
def conformance_report(self, name: str) -> dict[str, Any]:
|
|
rows = self._conn.execute(
|
|
"SELECT contract_version, payload_kind, passed, COUNT(*) AS count FROM conformance_checks WHERE contract_name = ? GROUP BY contract_version, payload_kind, passed",
|
|
(name,),
|
|
).fetchall()
|
|
return {
|
|
"contract": name,
|
|
"checks": [dict(row) for row in rows],
|
|
}
|
|
|
|
def _row_to_contract(self, row: sqlite3.Row) -> AdapterContract:
|
|
return AdapterContract(
|
|
name=row["name"],
|
|
version=row["version"],
|
|
adapter_type=row["adapter_type"],
|
|
input_schema=json.loads(row["input_schema"]),
|
|
output_schema=json.loads(row["output_schema"]),
|
|
capabilities=json.loads(row["capabilities"]),
|
|
metadata=json.loads(row["metadata"]),
|
|
)
|