relationship persistence, context entities, idempotent asset creation, audit/version handling for relationship changes

This commit is contained in:
2026-05-06 02:09:23 +02:00
parent bf59087073
commit 286ebc3cb6
12 changed files with 651 additions and 24 deletions

View File

@@ -10,6 +10,9 @@ from kontextual_engine.core import (
AssetRepresentation,
AssetVersion,
AuditEvent,
ContextEntity,
CoreRelationship,
IdempotencyRecord,
KnowledgeAsset,
LifecycleState,
MetadataRecord,
@@ -24,8 +27,11 @@ class InMemoryAssetRegistryRepository:
assets: dict[str, KnowledgeAsset] = field(default_factory=dict)
representations: dict[str, AssetRepresentation] = field(default_factory=dict)
metadata_records: dict[str, list[MetadataRecord]] = field(default_factory=dict)
context_entities: dict[str, ContextEntity] = field(default_factory=dict)
relationships: dict[str, CoreRelationship] = field(default_factory=dict)
versions: dict[str, list[AssetVersion]] = field(default_factory=dict)
audit_events: dict[str, AuditEvent] = field(default_factory=dict)
idempotency_records: dict[str, IdempotencyRecord] = field(default_factory=dict)
def save_actor(self, actor: Actor) -> Actor:
self.actors[actor.id] = actor
@@ -96,6 +102,50 @@ class InMemoryAssetRegistryRepository:
self.get_asset(asset_id)
return list(self.metadata_records.get(asset_id, []))
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
self.context_entities[entity.entity_id] = entity
return entity
def get_context_entity(self, entity_id: str) -> ContextEntity:
try:
return self.context_entities[entity_id]
except KeyError as exc:
raise NotFoundError("Context entity not found", details={"entity_id": entity_id}) from exc
def list_context_entities(self) -> list[ContextEntity]:
return sorted(self.context_entities.values(), key=lambda entity: (entity.entity_type.value, entity.name, entity.entity_id))
def save_relationship(self, relationship: CoreRelationship) -> CoreRelationship:
self.get_asset(relationship.source_id)
if relationship.target_kind.value == "asset":
self.get_asset(relationship.target_id)
else:
self.get_context_entity(relationship.target_id)
self.relationships[relationship.relationship_id] = relationship
return relationship
def get_relationship(self, relationship_id: str) -> CoreRelationship:
try:
return self.relationships[relationship_id]
except KeyError as exc:
raise NotFoundError(
"Relationship not found",
details={"relationship_id": relationship_id},
) from exc
def list_relationships(
self,
*,
source_id: str | None = None,
target_id: str | None = None,
) -> list[CoreRelationship]:
relationships: Iterable[CoreRelationship] = self.relationships.values()
if source_id is not None:
relationships = [item for item in relationships if item.source_id == source_id]
if target_id is not None:
relationships = [item for item in relationships if item.target_id == target_id]
return sorted(relationships, key=lambda item: (item.source_id, item.target_id, item.predicate, item.relationship_id))
def save_version(self, version: AssetVersion) -> AssetVersion:
self.get_asset(version.asset_id)
current = self.versions.setdefault(version.asset_id, [])
@@ -115,6 +165,12 @@ class InMemoryAssetRegistryRepository:
self.audit_events[event.event_id] = event
return event
def get_audit_event(self, event_id: str) -> AuditEvent:
try:
return self.audit_events[event_id]
except KeyError as exc:
raise NotFoundError("Audit event not found", details={"event_id": event_id}) from exc
def list_audit_events(
self,
*,
@@ -128,3 +184,9 @@ class InMemoryAssetRegistryRepository:
events = [event for event in events if event.correlation_id == correlation_id]
return sorted(events, key=lambda event: (event.occurred_at, event.event_id))
def save_idempotency_record(self, record: IdempotencyRecord) -> IdempotencyRecord:
self.idempotency_records[record.key] = record
return record
def get_idempotency_record(self, key: str) -> IdempotencyRecord | None:
return self.idempotency_records.get(key)

View File

@@ -12,10 +12,14 @@ from kontextual_engine.core import (
AssetRepresentation,
AssetVersion,
AuditEvent,
ContextEntity,
CoreRelationship,
IdempotencyRecord,
KnowledgeAsset,
LifecycleState,
MetadataRecord,
RepresentationKind,
RelationshipTargetKind,
)
from kontextual_engine.errors import NotFoundError, ValidationError
@@ -183,6 +187,90 @@ class SQLiteAssetRegistryRepository:
self.get_asset(asset_id)
return [MetadataRecord.from_dict(_loads(row["payload"])) for row in rows]
def save_context_entity(self, entity: ContextEntity) -> ContextEntity:
with self._connect() as conn:
conn.execute(
"""
insert into context_entities (id, entity_type, name, payload)
values (?, ?, ?, ?)
on conflict(id) do update set
entity_type=excluded.entity_type,
name=excluded.name,
payload=excluded.payload
""",
(entity.entity_id, entity.entity_type.value, entity.name, _json(entity.to_dict())),
)
return entity
def get_context_entity(self, entity_id: str) -> ContextEntity:
row = self._one("select payload from context_entities where id = ?", (entity_id,))
if row is None:
raise NotFoundError("Context entity not found", details={"entity_id": entity_id})
return ContextEntity.from_dict(_loads(row["payload"]))
def list_context_entities(self) -> list[ContextEntity]:
rows = self._all("select payload from context_entities order by entity_type, name, id", ())
return [ContextEntity.from_dict(_loads(row["payload"])) for row in rows]
def save_relationship(self, relationship: CoreRelationship) -> CoreRelationship:
self.get_asset(relationship.source_id)
if relationship.target_kind == RelationshipTargetKind.ASSET:
self.get_asset(relationship.target_id)
else:
self.get_context_entity(relationship.target_id)
with self._connect() as conn:
conn.execute(
"""
insert into core_relationships (id, source_id, target_id, target_kind, predicate, payload)
values (?, ?, ?, ?, ?, ?)
on conflict(id) do update set
source_id=excluded.source_id,
target_id=excluded.target_id,
target_kind=excluded.target_kind,
predicate=excluded.predicate,
payload=excluded.payload
""",
(
relationship.relationship_id,
relationship.source_id,
relationship.target_id,
relationship.target_kind.value,
relationship.predicate,
_json(relationship.to_dict()),
),
)
return relationship
def get_relationship(self, relationship_id: str) -> CoreRelationship:
row = self._one("select payload from core_relationships where id = ?", (relationship_id,))
if row is None:
raise NotFoundError(
"Relationship not found",
details={"relationship_id": relationship_id},
)
return CoreRelationship.from_dict(_loads(row["payload"]))
def list_relationships(
self,
*,
source_id: str | None = None,
target_id: str | None = None,
) -> list[CoreRelationship]:
clauses = []
params: list[Any] = []
if source_id is not None:
clauses.append("source_id = ?")
params.append(source_id)
if target_id is not None:
clauses.append("target_id = ?")
params.append(target_id)
where = f" where {' and '.join(clauses)}" if clauses else ""
rows = self._all(
f"select payload from core_relationships{where} order by source_id, target_id, predicate, id",
tuple(params),
)
return [CoreRelationship.from_dict(_loads(row["payload"])) for row in rows]
def save_version(self, version: AssetVersion) -> AssetVersion:
try:
with self._connect() as conn:
@@ -241,6 +329,12 @@ class SQLiteAssetRegistryRepository:
)
return event
def get_audit_event(self, event_id: str) -> AuditEvent:
row = self._one("select payload from audit_events where id = ?", (event_id,))
if row is None:
raise NotFoundError("Audit event not found", details={"event_id": event_id})
return AuditEvent.from_dict(_loads(row["payload"]))
def list_audit_events(
self,
*,
@@ -259,6 +353,34 @@ class SQLiteAssetRegistryRepository:
rows = self._all(f"select payload from audit_events{where} order by occurred_at, id", tuple(params))
return [AuditEvent.from_dict(_loads(row["payload"])) for row in rows]
def save_idempotency_record(self, record: IdempotencyRecord) -> IdempotencyRecord:
with self._connect() as conn:
conn.execute(
"""
insert into idempotency_records (key, operation, request_hash, status, payload)
values (?, ?, ?, ?, ?)
on conflict(key) do update set
operation=excluded.operation,
request_hash=excluded.request_hash,
status=excluded.status,
payload=excluded.payload
""",
(
record.key,
record.operation,
record.request_hash,
record.status.value,
_json(record.to_dict()),
),
)
return record
def get_idempotency_record(self, key: str) -> IdempotencyRecord | None:
row = self._one("select payload from idempotency_records where key = ?", (key,))
if row is None:
return None
return IdempotencyRecord.from_dict(_loads(row["payload"]))
def _initialize(self) -> None:
with self._connect() as conn:
conn.executescript(
@@ -288,6 +410,20 @@ class SQLiteAssetRegistryRepository:
key text not null,
payload text not null
);
create table if not exists context_entities (
id text primary key,
entity_type text not null,
name text not null,
payload text not null
);
create table if not exists core_relationships (
id text primary key,
source_id text not null references assets(id) on delete cascade,
target_id text not null,
target_kind text not null,
predicate text not null,
payload text not null
);
create table if not exists asset_versions (
id text primary key,
asset_id text not null references assets(id) on delete cascade,
@@ -306,9 +442,19 @@ class SQLiteAssetRegistryRepository:
payload text not null,
foreign key(actor_id) references actors(id)
);
create table if not exists idempotency_records (
key text primary key,
operation text not null,
request_hash text not null,
status text not null,
payload text not null
);
create index if not exists idx_assets_lifecycle on assets(lifecycle);
create index if not exists idx_representations_asset on representations(asset_id);
create index if not exists idx_metadata_asset on metadata_records(asset_id);
create index if not exists idx_entities_type on context_entities(entity_type);
create index if not exists idx_relationships_source on core_relationships(source_id);
create index if not exists idx_relationships_target on core_relationships(target_id);
create index if not exists idx_versions_asset on asset_versions(asset_id);
create index if not exists idx_audit_target on audit_events(target);
create index if not exists idx_audit_correlation on audit_events(correlation_id);