|
@@ -1,16 +1,69 @@
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
-from dataclasses import replace
|
|
|
|
|
|
|
+from dataclasses import asdict, replace
|
|
|
|
|
|
|
|
|
|
+from app.core.common.identifiers import new_governance_uid
|
|
|
from app.models.data_research import (
|
|
from app.models.data_research import (
|
|
|
|
|
+ CatalogSnapshot,
|
|
|
CandidateDecisionRecord,
|
|
CandidateDecisionRecord,
|
|
|
DataElement,
|
|
DataElement,
|
|
|
DataElementVersion,
|
|
DataElementVersion,
|
|
|
|
|
+ EvidenceFragment,
|
|
|
ExtractionCandidate,
|
|
ExtractionCandidate,
|
|
|
IngestionJob,
|
|
IngestionJob,
|
|
|
|
|
+ IngestionSource,
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class SqlAlchemyIngestionSourceRepository:
|
|
|
|
|
+ def __init__(self, session):
|
|
|
|
|
+ self.session = session
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def _record(model):
|
|
|
|
|
+ from app.core.data_research.sources import IngestionSourceRecord
|
|
|
|
|
+
|
|
|
|
|
+ return IngestionSourceRecord(
|
|
|
|
|
+ uid=str(model.uid),
|
|
|
|
|
+ source_type=model.source_type,
|
|
|
|
|
+ name=model.name,
|
|
|
|
|
+ config=dict(model.config or {}),
|
|
|
|
|
+ permission_scope=dict(model.permission_scope or {}),
|
|
|
|
|
+ status=model.status,
|
|
|
|
|
+ created_by=model.created_by,
|
|
|
|
|
+ created_at=model.created_at,
|
|
|
|
|
+ updated_at=model.updated_at,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def get(self, uid):
|
|
|
|
|
+ model = self.session.get(IngestionSource, str(uid))
|
|
|
|
|
+ return self._record(model) if model is not None else None
|
|
|
|
|
+
|
|
|
|
|
+ def save(self, record):
|
|
|
|
|
+ model = self.session.get(IngestionSource, str(record.uid))
|
|
|
|
|
+ if model is None:
|
|
|
|
|
+ model = IngestionSource(
|
|
|
|
|
+ uid=record.uid,
|
|
|
|
|
+ source_type=record.source_type,
|
|
|
|
|
+ name=record.name,
|
|
|
|
|
+ config=record.config,
|
|
|
|
|
+ permission_scope=record.permission_scope,
|
|
|
|
|
+ status=record.status,
|
|
|
|
|
+ created_by=record.created_by,
|
|
|
|
|
+ created_at=record.created_at,
|
|
|
|
|
+ updated_at=record.updated_at,
|
|
|
|
|
+ )
|
|
|
|
|
+ self.session.add(model)
|
|
|
|
|
+ else:
|
|
|
|
|
+ model.name = record.name
|
|
|
|
|
+ model.config = record.config
|
|
|
|
|
+ model.permission_scope = record.permission_scope
|
|
|
|
|
+ model.status = record.status
|
|
|
|
|
+ model.updated_at = record.updated_at
|
|
|
|
|
+ self.session.flush()
|
|
|
|
|
+ return self._record(model)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class SqlAlchemyIngestionJobRepository:
|
|
class SqlAlchemyIngestionJobRepository:
|
|
|
def __init__(self, session):
|
|
def __init__(self, session):
|
|
|
self.session = session
|
|
self.session = session
|
|
@@ -31,6 +84,8 @@ class SqlAlchemyIngestionJobRepository:
|
|
|
status=model.status,
|
|
status=model.status,
|
|
|
statistics=dict(model.statistics or {}),
|
|
statistics=dict(model.statistics or {}),
|
|
|
last_error=model.last_error,
|
|
last_error=model.last_error,
|
|
|
|
|
+ attempt_count=int(model.attempt_count or 0),
|
|
|
|
|
+ failure_stage=model.failure_stage,
|
|
|
force_rerun=bool(model.force_rerun),
|
|
force_rerun=bool(model.force_rerun),
|
|
|
created_at=model.created_at,
|
|
created_at=model.created_at,
|
|
|
updated_at=model.updated_at,
|
|
updated_at=model.updated_at,
|
|
@@ -74,6 +129,8 @@ class SqlAlchemyIngestionJobRepository:
|
|
|
parameters=record.parameters,
|
|
parameters=record.parameters,
|
|
|
statistics=record.statistics,
|
|
statistics=record.statistics,
|
|
|
last_error=record.last_error,
|
|
last_error=record.last_error,
|
|
|
|
|
+ attempt_count=record.attempt_count,
|
|
|
|
|
+ failure_stage=record.failure_stage,
|
|
|
actor_uid=record.actor_uid,
|
|
actor_uid=record.actor_uid,
|
|
|
force_rerun=record.force_rerun,
|
|
force_rerun=record.force_rerun,
|
|
|
started_at=record.started_at,
|
|
started_at=record.started_at,
|
|
@@ -91,6 +148,8 @@ class SqlAlchemyIngestionJobRepository:
|
|
|
"status",
|
|
"status",
|
|
|
"statistics",
|
|
"statistics",
|
|
|
"last_error",
|
|
"last_error",
|
|
|
|
|
+ "attempt_count",
|
|
|
|
|
+ "failure_stage",
|
|
|
"started_at",
|
|
"started_at",
|
|
|
"finished_at",
|
|
"finished_at",
|
|
|
"updated_at",
|
|
"updated_at",
|
|
@@ -100,6 +159,88 @@ class SqlAlchemyIngestionJobRepository:
|
|
|
return self._record(model)
|
|
return self._record(model)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+class SqlAlchemyCatalogSnapshotRepository:
|
|
|
|
|
+ def __init__(self, session, *, uid_factory=new_governance_uid):
|
|
|
|
|
+ self.session = session
|
|
|
|
|
+ self.uid_factory = uid_factory
|
|
|
|
|
+
|
|
|
|
|
+ def _record(self, model):
|
|
|
|
|
+ from app.core.data_research.catalog.models import CatalogSnapshotRecord
|
|
|
|
|
+
|
|
|
|
|
+ evidence_count = self.session.query(EvidenceFragment).filter_by(
|
|
|
|
|
+ job_uid=str(model.job_uid)
|
|
|
|
|
+ ).filter(
|
|
|
|
|
+ EvidenceFragment.locator["attempt"].astext
|
|
|
|
|
+ == str(model.attempt)
|
|
|
|
|
+ ).count()
|
|
|
|
|
+ return CatalogSnapshotRecord(
|
|
|
|
|
+ uid=str(model.uid),
|
|
|
|
|
+ job_uid=str(model.job_uid),
|
|
|
|
|
+ source_uid=str(model.source_uid),
|
|
|
|
|
+ attempt=int(model.attempt),
|
|
|
|
|
+ database_type=model.database_type,
|
|
|
|
|
+ content_hash=model.content_hash,
|
|
|
|
|
+ snapshot=dict(model.snapshot or {}),
|
|
|
|
|
+ evidence_count=int(evidence_count),
|
|
|
|
|
+ created_at=model.created_at,
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ def find(self, job_uid, attempt):
|
|
|
|
|
+ model = self.session.query(CatalogSnapshot).filter_by(
|
|
|
|
|
+ job_uid=str(job_uid),
|
|
|
|
|
+ attempt=int(attempt),
|
|
|
|
|
+ ).first()
|
|
|
|
|
+ return self._record(model) if model is not None else None
|
|
|
|
|
+
|
|
|
|
|
+ def persist(self, job_uid, attempt, snapshot):
|
|
|
|
|
+ existing = self.find(job_uid, attempt)
|
|
|
|
|
+ if existing is not None:
|
|
|
|
|
+ return existing
|
|
|
|
|
+ model = CatalogSnapshot(
|
|
|
|
|
+ uid=self.uid_factory(),
|
|
|
|
|
+ job_uid=str(job_uid),
|
|
|
|
|
+ source_uid=str(snapshot.data_source_uid),
|
|
|
|
|
+ attempt=int(attempt),
|
|
|
|
|
+ database_type=snapshot.database_type,
|
|
|
|
|
+ content_hash=snapshot.content_hash,
|
|
|
|
|
+ snapshot=asdict(snapshot),
|
|
|
|
|
+ )
|
|
|
|
|
+ self.session.add(model)
|
|
|
|
|
+ for asset in snapshot.assets:
|
|
|
|
|
+ for field in asset.fields:
|
|
|
|
|
+ self.session.add(
|
|
|
|
|
+ EvidenceFragment(
|
|
|
|
|
+ uid=self.uid_factory(),
|
|
|
|
|
+ job_uid=str(job_uid),
|
|
|
|
|
+ locator={
|
|
|
|
|
+ "kind": "database.column",
|
|
|
|
|
+ "data_source_uid": str(snapshot.data_source_uid),
|
|
|
|
|
+ "database_type": snapshot.database_type,
|
|
|
|
|
+ "schema": field.schema,
|
|
|
|
|
+ "table": field.asset,
|
|
|
|
|
+ "column": field.name,
|
|
|
|
|
+ "ordinal_position": int(field.ordinal_position),
|
|
|
|
|
+ "attempt": int(attempt),
|
|
|
|
|
+ },
|
|
|
|
|
+ excerpt=(
|
|
|
|
|
+ f"{field.schema}.{field.asset}.{field.name} "
|
|
|
|
|
+ f"{field.data_type}"
|
|
|
|
|
+ ),
|
|
|
|
|
+ confidence=1.0,
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ self.session.flush()
|
|
|
|
|
+ return self._record(model)
|
|
|
|
|
+
|
|
|
|
|
+ def list(self, job_uid):
|
|
|
|
|
+ return [
|
|
|
|
|
+ self._record(model)
|
|
|
|
|
+ for model in self.session.query(CatalogSnapshot).filter_by(
|
|
|
|
|
+ job_uid=str(job_uid)
|
|
|
|
|
+ ).order_by(CatalogSnapshot.attempt.desc()).all()
|
|
|
|
|
+ ]
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
class SqlAlchemyDataElementRepository:
|
|
class SqlAlchemyDataElementRepository:
|
|
|
def __init__(self, session):
|
|
def __init__(self, session):
|
|
|
self.session = session
|
|
self.session = session
|