|
|
@@ -0,0 +1,243 @@
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import hashlib
|
|
|
+import json
|
|
|
+from dataclasses import replace
|
|
|
+
|
|
|
+from app.core.common.identifiers import new_governance_uid
|
|
|
+from app.core.data_research.ontology.models import (
|
|
|
+ DomainLink,
|
|
|
+ GraphDocument,
|
|
|
+ Ontology,
|
|
|
+ OntologyVersion,
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+class OntologyRepositoryError(ValueError):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class OntologyConflict(OntologyRepositoryError):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+class OntologyImmutable(OntologyRepositoryError):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+def canonical_graph_hash(graph):
|
|
|
+ document = GraphDocument.from_dict(graph).to_dict()
|
|
|
+ encoded = json.dumps(document, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
|
+ return hashlib.sha256(encoded.encode("utf-8")).hexdigest()
|
|
|
+
|
|
|
+
|
|
|
+class MemoryOntologyRepository:
|
|
|
+ def __init__(self, *, uid_factory=new_governance_uid):
|
|
|
+ self.uid_factory = uid_factory
|
|
|
+ self.ontologies = {}
|
|
|
+ self.versions = {}
|
|
|
+
|
|
|
+ def create(self, *, code, name, owner_uid, domain_links, created_by=None):
|
|
|
+ if any(item.code == code for item in self.ontologies.values()):
|
|
|
+ raise OntologyConflict("ontology code already exists")
|
|
|
+ links = tuple(DomainLink(str(item["domain_uid"]), str(item["role"])) for item in domain_links)
|
|
|
+ ontology = Ontology(
|
|
|
+ uid=self.uid_factory(),
|
|
|
+ code=str(code),
|
|
|
+ name=str(name),
|
|
|
+ owner_uid=str(owner_uid),
|
|
|
+ domain_links=links,
|
|
|
+ )
|
|
|
+ self.ontologies[ontology.uid] = ontology
|
|
|
+ return ontology
|
|
|
+
|
|
|
+ def get(self, uid):
|
|
|
+ return self.ontologies.get(str(uid))
|
|
|
+
|
|
|
+ def get_version(self, uid):
|
|
|
+ return self.versions.get(str(uid))
|
|
|
+
|
|
|
+ def save_draft(self, ontology_uid, graph, *, expected_revision, actor_uid):
|
|
|
+ ontology = self.get(ontology_uid)
|
|
|
+ if ontology is None:
|
|
|
+ raise LookupError("ontology was not found")
|
|
|
+ if ontology.draft_revision != int(expected_revision):
|
|
|
+ raise OntologyConflict("ontology draft revision conflict")
|
|
|
+ version_number = 1 + max(
|
|
|
+ (item.version for item in self.versions.values() if item.ontology_uid == ontology.uid),
|
|
|
+ default=0,
|
|
|
+ )
|
|
|
+ version = OntologyVersion(
|
|
|
+ uid=self.uid_factory(),
|
|
|
+ ontology_uid=ontology.uid,
|
|
|
+ version=version_number,
|
|
|
+ parent_version_uid=ontology.active_version_uid,
|
|
|
+ graph_document=GraphDocument.from_dict(graph),
|
|
|
+ content_hash=canonical_graph_hash(graph),
|
|
|
+ created_by=actor_uid,
|
|
|
+ )
|
|
|
+ self.versions[version.uid] = version
|
|
|
+ self.ontologies[ontology.uid] = replace(ontology, draft_revision=ontology.draft_revision + 1)
|
|
|
+ return version
|
|
|
+
|
|
|
+ def mark_published(self, version_uid):
|
|
|
+ version = self.get_version(version_uid)
|
|
|
+ if version is None:
|
|
|
+ raise LookupError("ontology version was not found")
|
|
|
+ published = replace(version, status="published")
|
|
|
+ self.versions[published.uid] = published
|
|
|
+ ontology = self.get(published.ontology_uid)
|
|
|
+ self.ontologies[ontology.uid] = replace(
|
|
|
+ ontology, status="published", active_version_uid=published.uid
|
|
|
+ )
|
|
|
+ return published
|
|
|
+
|
|
|
+ def replace_version(self, version_uid, graph):
|
|
|
+ version = self.get_version(version_uid)
|
|
|
+ if version.status == "published":
|
|
|
+ raise OntologyImmutable("published ontology version is immutable")
|
|
|
+ updated = replace(
|
|
|
+ version,
|
|
|
+ graph_document=GraphDocument.from_dict(graph),
|
|
|
+ content_hash=canonical_graph_hash(graph),
|
|
|
+ )
|
|
|
+ self.versions[updated.uid] = updated
|
|
|
+ return updated
|
|
|
+
|
|
|
+
|
|
|
+class SqlAlchemyOntologyRepository:
|
|
|
+ def __init__(self, session, *, uid_factory=new_governance_uid):
|
|
|
+ self.session = session
|
|
|
+ self.uid_factory = uid_factory
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _version(model):
|
|
|
+ return OntologyVersion(
|
|
|
+ uid=str(model.uid),
|
|
|
+ ontology_uid=str(model.ontology_uid),
|
|
|
+ version=int(model.version),
|
|
|
+ parent_version_uid=str(model.parent_version_uid) if model.parent_version_uid else None,
|
|
|
+ status=model.status,
|
|
|
+ graph_document=GraphDocument.from_dict(model.graph_document),
|
|
|
+ content_hash=model.content_hash,
|
|
|
+ created_by=model.created_by,
|
|
|
+ )
|
|
|
+
|
|
|
+ def _ontology(self, model):
|
|
|
+ from app.models.data_research import OntologyDomainLinkModel
|
|
|
+
|
|
|
+ links = self.session.query(OntologyDomainLinkModel).filter_by(
|
|
|
+ ontology_uid=str(model.uid)
|
|
|
+ ).all()
|
|
|
+ return Ontology(
|
|
|
+ uid=str(model.uid),
|
|
|
+ code=model.code,
|
|
|
+ name=model.name,
|
|
|
+ owner_uid=model.owner_uid,
|
|
|
+ status=model.status,
|
|
|
+ draft_revision=int(model.draft_revision),
|
|
|
+ active_version_uid=str(model.active_version_uid) if model.active_version_uid else None,
|
|
|
+ domain_links=tuple(DomainLink(str(item.domain_uid), item.role) for item in links),
|
|
|
+ )
|
|
|
+
|
|
|
+ def list(self):
|
|
|
+ from app.models.data_research import OntologyModel
|
|
|
+
|
|
|
+ return [self._ontology(model) for model in self.session.query(OntologyModel).order_by(OntologyModel.code).all()]
|
|
|
+
|
|
|
+ def create(self, *, code, name, owner_uid, domain_links, created_by=None):
|
|
|
+ from app.models.data_research import OntologyDomainLinkModel, OntologyModel
|
|
|
+
|
|
|
+ if self.session.query(OntologyModel).filter_by(code=str(code)).first():
|
|
|
+ raise OntologyConflict("ontology code already exists")
|
|
|
+ uid = self.uid_factory()
|
|
|
+ model = OntologyModel(
|
|
|
+ uid=uid,
|
|
|
+ code=str(code),
|
|
|
+ name=str(name),
|
|
|
+ owner_uid=str(owner_uid),
|
|
|
+ created_by=created_by,
|
|
|
+ )
|
|
|
+ self.session.add(model)
|
|
|
+ for link in domain_links:
|
|
|
+ self.session.add(
|
|
|
+ OntologyDomainLinkModel(
|
|
|
+ ontology_uid=uid,
|
|
|
+ domain_uid=str(link["domain_uid"]),
|
|
|
+ role=str(link["role"]),
|
|
|
+ )
|
|
|
+ )
|
|
|
+ self.session.flush()
|
|
|
+ return self._ontology(model)
|
|
|
+
|
|
|
+ def get(self, uid):
|
|
|
+ from app.models.data_research import OntologyModel
|
|
|
+
|
|
|
+ model = self.session.get(OntologyModel, str(uid))
|
|
|
+ return self._ontology(model) if model is not None else None
|
|
|
+
|
|
|
+ def get_version(self, uid):
|
|
|
+ from app.models.data_research import OntologyVersionModel
|
|
|
+
|
|
|
+ model = self.session.get(OntologyVersionModel, str(uid))
|
|
|
+ return self._version(model) if model is not None else None
|
|
|
+
|
|
|
+ def latest_version(self, ontology_uid):
|
|
|
+ from app.models.data_research import OntologyVersionModel
|
|
|
+
|
|
|
+ model = self.session.query(OntologyVersionModel).filter_by(
|
|
|
+ ontology_uid=str(ontology_uid)
|
|
|
+ ).order_by(OntologyVersionModel.version.desc()).first()
|
|
|
+ return self._version(model) if model is not None else None
|
|
|
+
|
|
|
+ def save_draft(self, ontology_uid, graph, *, expected_revision, actor_uid):
|
|
|
+ from app.models.data_research import OntologyModel, OntologyVersionModel
|
|
|
+
|
|
|
+ model = self.session.get(OntologyModel, str(ontology_uid))
|
|
|
+ if model is None:
|
|
|
+ raise LookupError("ontology was not found")
|
|
|
+ if int(model.draft_revision) != int(expected_revision):
|
|
|
+ raise OntologyConflict("ontology draft revision conflict")
|
|
|
+ latest = self.latest_version(ontology_uid)
|
|
|
+ version = OntologyVersionModel(
|
|
|
+ uid=self.uid_factory(),
|
|
|
+ ontology_uid=str(ontology_uid),
|
|
|
+ version=(latest.version + 1) if latest else 1,
|
|
|
+ parent_version_uid=model.active_version_uid,
|
|
|
+ status="draft",
|
|
|
+ graph_document=GraphDocument.from_dict(graph).to_dict(),
|
|
|
+ content_hash=canonical_graph_hash(graph),
|
|
|
+ created_by=actor_uid,
|
|
|
+ )
|
|
|
+ model.draft_revision = int(model.draft_revision) + 1
|
|
|
+ self.session.add(version)
|
|
|
+ self.session.flush()
|
|
|
+ return self._version(version)
|
|
|
+
|
|
|
+ def mark_published(self, version_uid):
|
|
|
+ from app.models.data_research import OntologyModel, OntologyVersionModel
|
|
|
+
|
|
|
+ model = self.session.get(OntologyVersionModel, str(version_uid))
|
|
|
+ if model is None:
|
|
|
+ raise LookupError("ontology version was not found")
|
|
|
+ ontology = self.session.get(OntologyModel, str(model.ontology_uid))
|
|
|
+ if ontology.active_version_uid:
|
|
|
+ active = self.session.get(OntologyVersionModel, str(ontology.active_version_uid))
|
|
|
+ if active is not None:
|
|
|
+ active.status = "superseded"
|
|
|
+ model.status = "published"
|
|
|
+ ontology.status = "published"
|
|
|
+ ontology.active_version_uid = model.uid
|
|
|
+ self.session.flush()
|
|
|
+ return self._version(model)
|
|
|
+
|
|
|
+ def replace_version(self, version_uid, graph):
|
|
|
+ from app.models.data_research import OntologyVersionModel
|
|
|
+
|
|
|
+ model = self.session.get(OntologyVersionModel, str(version_uid))
|
|
|
+ if model.status == "published":
|
|
|
+ raise OntologyImmutable("published ontology version is immutable")
|
|
|
+ model.graph_document = GraphDocument.from_dict(graph).to_dict()
|
|
|
+ model.content_hash = canonical_graph_hash(graph)
|
|
|
+ self.session.flush()
|
|
|
+ return self._version(model)
|