mirror of
https://github.com/mcloh/Enterprise-MCP-Service-Bus.git
synced 2026-11-05 22:14:11 +00:00
108 lines
4.1 KiB
Python
108 lines
4.1 KiB
Python
"""Global Capability Registry storage (EP-02-T03, README.md §6.6, ADR-010).
|
|
|
|
A control-plane asset, deliberately separate from the per-client catalog
|
|
view the Gateway serves (README.md §6.6: "O Global Registry é um
|
|
control-plane asset. Ele não deve ser confundido com a visão de catálogo
|
|
entregue a cada client."). Consumers (Gateway, Fabric, Offering Filter) only
|
|
ever read through `list_active`/`get` -- nothing outside the publishing
|
|
pipeline (EP-02-T02) writes here, and the pipeline never writes directly:
|
|
it only transitions status through `lifecycle.py`.
|
|
|
|
SQLite, used synchronously: this store is not on the Gateway's request
|
|
hot path in this milestone (routing is still static, EP-06 wires it in
|
|
later), and SQLite operations here are single-digit milliseconds -- an
|
|
`asyncio.to_thread` wrapper is a straightforward addition if/when a caller
|
|
needs this off the event loop.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sqlite3
|
|
from collections.abc import Iterator
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
from emcp_bus.registry.models import CapabilityManifest, LifecycleStatus
|
|
|
|
|
|
class CapabilityNotFoundError(Exception):
|
|
pass
|
|
|
|
|
|
class RegistryStore:
|
|
def __init__(self, db_path: str | Path) -> None:
|
|
self._db_path = str(db_path)
|
|
with self._connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS capabilities (
|
|
name TEXT PRIMARY KEY,
|
|
domain TEXT NOT NULL,
|
|
manifest_json TEXT NOT NULL,
|
|
lifecycle_status TEXT NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
|
|
@contextmanager
|
|
def _connect(self) -> Iterator[sqlite3.Connection]:
|
|
conn = sqlite3.connect(self._db_path)
|
|
try:
|
|
yield conn
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
def upsert(self, manifest: CapabilityManifest) -> None:
|
|
with self._connect() as conn:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO capabilities (name, domain, manifest_json, lifecycle_status)
|
|
VALUES (?, ?, ?, ?)
|
|
ON CONFLICT(name) DO UPDATE SET
|
|
domain = excluded.domain,
|
|
manifest_json = excluded.manifest_json,
|
|
lifecycle_status = excluded.lifecycle_status
|
|
""",
|
|
(
|
|
manifest.name,
|
|
manifest.domain,
|
|
manifest.model_dump_json(),
|
|
manifest.lifecycle.status.value,
|
|
),
|
|
)
|
|
|
|
def get(self, name: str) -> CapabilityManifest:
|
|
with self._connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT manifest_json FROM capabilities WHERE name = ?", (name,)
|
|
).fetchone()
|
|
if row is None:
|
|
raise CapabilityNotFoundError(name)
|
|
return CapabilityManifest.model_validate_json(row[0])
|
|
|
|
def list_by_domain(self, domain: str) -> list[CapabilityManifest]:
|
|
with self._connect() as conn:
|
|
rows = conn.execute(
|
|
"SELECT manifest_json FROM capabilities WHERE domain = ? ORDER BY name", (domain,)
|
|
).fetchall()
|
|
return [CapabilityManifest.model_validate_json(row[0]) for row in rows]
|
|
|
|
def list_active(self) -> list[CapabilityManifest]:
|
|
"""What Gateway/Fabric/Offering Filter are allowed to read (README.md §6.6) --
|
|
anything still in nominate/review/register, or already deprecated/retired,
|
|
is a control-plane-only concern."""
|
|
with self._connect() as conn:
|
|
rows = conn.execute(
|
|
"SELECT manifest_json FROM capabilities WHERE lifecycle_status = ? ORDER BY name",
|
|
(LifecycleStatus.ACTIVE.value,),
|
|
).fetchall()
|
|
return [CapabilityManifest.model_validate_json(row[0]) for row in rows]
|
|
|
|
def set_lifecycle_status(self, name: str, status: LifecycleStatus) -> None:
|
|
manifest = self.get(name)
|
|
updated = manifest.model_copy(
|
|
update={"lifecycle": manifest.lifecycle.model_copy(update={"status": status})}
|
|
)
|
|
self.upsert(updated)
|