Initial import NDC_1C
This commit is contained in:
@@ -0,0 +1,744 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy import create_engine, delete, func, select
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from canonical_layer.models import CanonicalEntity
|
||||
from canonical_layer.store_models import (
|
||||
AnomalySignalRow,
|
||||
Base,
|
||||
CanonicalEntityRow,
|
||||
CanonicalLinkRow,
|
||||
FeatureMetricRow,
|
||||
FeatureRunRow,
|
||||
RefreshCheckpointRow,
|
||||
RefreshRunRow,
|
||||
RiskPatternRow,
|
||||
RiskRunRow,
|
||||
)
|
||||
|
||||
|
||||
def _utc_now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
def _dump_json(payload: Any) -> str:
|
||||
return json.dumps(payload, ensure_ascii=False)
|
||||
|
||||
|
||||
def _load_json(payload: str, default: Any) -> Any:
|
||||
if not payload:
|
||||
return default
|
||||
try:
|
||||
return json.loads(payload)
|
||||
except json.JSONDecodeError:
|
||||
return default
|
||||
|
||||
|
||||
def _dt_to_iso(value: datetime | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=timezone.utc)
|
||||
return value.astimezone(timezone.utc).isoformat()
|
||||
|
||||
|
||||
class CanonicalStore:
|
||||
def __init__(self, db_url: str) -> None:
|
||||
self.db_url = db_url
|
||||
self.engine = create_engine(db_url, future=True)
|
||||
self.session_factory = sessionmaker(bind=self.engine, autoflush=False, expire_on_commit=False, future=True)
|
||||
|
||||
def _ensure_sqlite_path(self) -> None:
|
||||
if not self.db_url.startswith("sqlite:///"):
|
||||
return
|
||||
db_path_raw = self.db_url.replace("sqlite:///", "", 1)
|
||||
db_path = Path(db_path_raw)
|
||||
db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def ensure_created(self) -> None:
|
||||
self._ensure_sqlite_path()
|
||||
Base.metadata.create_all(self.engine)
|
||||
|
||||
def _session(self) -> Session:
|
||||
return self.session_factory()
|
||||
|
||||
def start_refresh_run(
|
||||
self,
|
||||
*,
|
||||
mode: str,
|
||||
requested_entity_sets: list[str],
|
||||
date_from: str | None,
|
||||
date_to: str | None,
|
||||
limit_per_set: int,
|
||||
) -> str:
|
||||
run_id = uuid4().hex
|
||||
with self._session() as session, session.begin():
|
||||
session.add(
|
||||
RefreshRunRow(
|
||||
id=run_id,
|
||||
mode=mode,
|
||||
status="running",
|
||||
started_at=_utc_now(),
|
||||
requested_entity_sets_json=_dump_json(requested_entity_sets),
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
limit_per_set=limit_per_set,
|
||||
)
|
||||
)
|
||||
return run_id
|
||||
|
||||
def finish_refresh_run(
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
status: str,
|
||||
records_read: int,
|
||||
entities_written: int,
|
||||
links_written: int,
|
||||
checkpoints_updated: int,
|
||||
details: dict[str, Any] | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
with self._session() as session, session.begin():
|
||||
run = session.get(RefreshRunRow, run_id)
|
||||
if run is None:
|
||||
return
|
||||
run.status = status
|
||||
run.records_read = records_read
|
||||
run.entities_written = entities_written
|
||||
run.links_written = links_written
|
||||
run.checkpoints_updated = checkpoints_updated
|
||||
run.details_json = _dump_json(details or {})
|
||||
run.error_message = error_message
|
||||
run.finished_at = _utc_now()
|
||||
|
||||
def upsert_entities(self, *, run_id: str, entities: list[CanonicalEntity]) -> tuple[int, int]:
|
||||
entities_written = 0
|
||||
links_written = 0
|
||||
now = _utc_now()
|
||||
|
||||
with self._session() as session, session.begin():
|
||||
for entity in entities:
|
||||
row = session.execute(
|
||||
select(CanonicalEntityRow).where(
|
||||
CanonicalEntityRow.source_entity == entity.source_entity,
|
||||
CanonicalEntityRow.source_id == entity.source_id,
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if row is None:
|
||||
row = CanonicalEntityRow(
|
||||
source_entity=entity.source_entity,
|
||||
source_id=entity.source_id,
|
||||
display_name=entity.display_name,
|
||||
attributes_json=_dump_json(entity.attributes),
|
||||
first_seen_at=now,
|
||||
updated_at=now,
|
||||
last_refresh_run_id=run_id,
|
||||
)
|
||||
session.add(row)
|
||||
else:
|
||||
row.display_name = entity.display_name
|
||||
row.attributes_json = _dump_json(entity.attributes)
|
||||
row.updated_at = now
|
||||
row.last_refresh_run_id = run_id
|
||||
|
||||
entities_written += 1
|
||||
|
||||
session.execute(
|
||||
delete(CanonicalLinkRow).where(
|
||||
CanonicalLinkRow.source_entity == entity.source_entity,
|
||||
CanonicalLinkRow.source_id == entity.source_id,
|
||||
)
|
||||
)
|
||||
|
||||
for link in entity.links:
|
||||
if not link.target_id:
|
||||
continue
|
||||
session.add(
|
||||
CanonicalLinkRow(
|
||||
source_entity=entity.source_entity,
|
||||
source_id=entity.source_id,
|
||||
relation=link.relation,
|
||||
target_entity=link.target_entity,
|
||||
target_id=link.target_id,
|
||||
source_field=link.source_field,
|
||||
updated_at=now,
|
||||
last_refresh_run_id=run_id,
|
||||
)
|
||||
)
|
||||
links_written += 1
|
||||
|
||||
return entities_written, links_written
|
||||
|
||||
def update_checkpoints(
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
entity_sets: list[str],
|
||||
date_from: str | None,
|
||||
date_to: str | None,
|
||||
) -> int:
|
||||
if not entity_sets:
|
||||
return 0
|
||||
|
||||
now = _utc_now()
|
||||
updated = 0
|
||||
with self._session() as session, session.begin():
|
||||
for entity_set in entity_sets:
|
||||
row = session.get(RefreshCheckpointRow, entity_set)
|
||||
if row is None:
|
||||
row = RefreshCheckpointRow(
|
||||
entity_set=entity_set,
|
||||
last_success_at=now,
|
||||
last_refresh_run_id=run_id,
|
||||
last_date_from=date_from,
|
||||
last_date_to=date_to,
|
||||
)
|
||||
session.add(row)
|
||||
else:
|
||||
row.last_success_at = now
|
||||
row.last_refresh_run_id = run_id
|
||||
row.last_date_from = date_from
|
||||
row.last_date_to = date_to
|
||||
updated += 1
|
||||
return updated
|
||||
|
||||
def list_recent_runs(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 200))
|
||||
with self._session() as session:
|
||||
rows = (
|
||||
session.execute(
|
||||
select(RefreshRunRow)
|
||||
.order_by(RefreshRunRow.started_at.desc())
|
||||
.limit(safe_limit)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"run_id": row.id,
|
||||
"mode": row.mode,
|
||||
"status": row.status,
|
||||
"started_at": _dt_to_iso(row.started_at),
|
||||
"finished_at": _dt_to_iso(row.finished_at),
|
||||
"requested_entity_sets": _load_json(row.requested_entity_sets_json, []),
|
||||
"date_from": row.date_from,
|
||||
"date_to": row.date_to,
|
||||
"limit_per_set": row.limit_per_set,
|
||||
"records_read": row.records_read,
|
||||
"entities_written": row.entities_written,
|
||||
"links_written": row.links_written,
|
||||
"checkpoints_updated": row.checkpoints_updated,
|
||||
"details": _load_json(row.details_json, {}),
|
||||
"error_message": row.error_message,
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def store_stats(self) -> dict[str, Any]:
|
||||
with self._session() as session:
|
||||
entities_total = session.execute(select(func.count(CanonicalEntityRow.id))).scalar_one()
|
||||
links_total = session.execute(select(func.count(CanonicalLinkRow.id))).scalar_one()
|
||||
checkpoints_total = session.execute(select(func.count(RefreshCheckpointRow.entity_set))).scalar_one()
|
||||
latest_run = (
|
||||
session.execute(select(RefreshRunRow).order_by(RefreshRunRow.started_at.desc()).limit(1))
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
|
||||
latest_run_payload: dict[str, Any] | None = None
|
||||
if latest_run is not None:
|
||||
latest_run_payload = {
|
||||
"run_id": latest_run.id,
|
||||
"mode": latest_run.mode,
|
||||
"status": latest_run.status,
|
||||
"started_at": _dt_to_iso(latest_run.started_at),
|
||||
"finished_at": _dt_to_iso(latest_run.finished_at),
|
||||
}
|
||||
|
||||
return {
|
||||
"db_url": self.db_url,
|
||||
"entities_total": int(entities_total),
|
||||
"links_total": int(links_total),
|
||||
"checkpoints_total": int(checkpoints_total),
|
||||
"latest_run": latest_run_payload,
|
||||
}
|
||||
|
||||
def start_feature_run(self, *, baseline_window_hours: int) -> str:
|
||||
run_id = uuid4().hex
|
||||
with self._session() as session, session.begin():
|
||||
session.add(
|
||||
FeatureRunRow(
|
||||
id=run_id,
|
||||
status="running",
|
||||
started_at=_utc_now(),
|
||||
baseline_window_hours=baseline_window_hours,
|
||||
)
|
||||
)
|
||||
return run_id
|
||||
|
||||
def replace_feature_results(
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
metrics: list[dict[str, Any]],
|
||||
anomalies: list[dict[str, Any]],
|
||||
) -> tuple[int, int]:
|
||||
now = _utc_now()
|
||||
with self._session() as session, session.begin():
|
||||
session.execute(delete(FeatureMetricRow).where(FeatureMetricRow.feature_run_id == run_id))
|
||||
session.execute(delete(AnomalySignalRow).where(AnomalySignalRow.feature_run_id == run_id))
|
||||
|
||||
# Deactivate previously active anomalies before writing a new active snapshot.
|
||||
previous_anomalies = session.execute(
|
||||
select(AnomalySignalRow).where(AnomalySignalRow.is_active == 1)
|
||||
).scalars().all()
|
||||
for item in previous_anomalies:
|
||||
item.is_active = 0
|
||||
|
||||
for metric in metrics:
|
||||
session.add(
|
||||
FeatureMetricRow(
|
||||
feature_run_id=run_id,
|
||||
metric_key=str(metric.get("metric_key", "")),
|
||||
scope=str(metric.get("scope", "global")),
|
||||
scope_id=str(metric.get("scope_id", "")),
|
||||
metric_type=str(metric.get("metric_type", "gauge")),
|
||||
metric_value=float(metric.get("metric_value", 0.0)),
|
||||
attributes_json=_dump_json(metric.get("attributes", {})),
|
||||
computed_at=now,
|
||||
)
|
||||
)
|
||||
|
||||
for anomaly in anomalies:
|
||||
session.add(
|
||||
AnomalySignalRow(
|
||||
feature_run_id=run_id,
|
||||
signal_type=str(anomaly.get("signal_type", "unknown_signal")),
|
||||
severity=str(anomaly.get("severity", "medium")),
|
||||
scope=str(anomaly.get("scope", "global")),
|
||||
scope_id=str(anomaly.get("scope_id", "")),
|
||||
score=float(anomaly.get("score", 0.0)),
|
||||
details_json=_dump_json(anomaly.get("details", {})),
|
||||
detected_at=now,
|
||||
is_active=1,
|
||||
)
|
||||
)
|
||||
|
||||
return len(metrics), len(anomalies)
|
||||
|
||||
def finish_feature_run(
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
status: str,
|
||||
entities_total: int,
|
||||
metrics_written: int,
|
||||
anomalies_written: int,
|
||||
details: dict[str, Any] | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
with self._session() as session, session.begin():
|
||||
row = session.get(FeatureRunRow, run_id)
|
||||
if row is None:
|
||||
return
|
||||
row.status = status
|
||||
row.entities_total = entities_total
|
||||
row.metrics_written = metrics_written
|
||||
row.anomalies_written = anomalies_written
|
||||
row.details_json = _dump_json(details or {})
|
||||
row.error_message = error_message
|
||||
row.finished_at = _utc_now()
|
||||
|
||||
def list_recent_feature_runs(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 200))
|
||||
with self._session() as session:
|
||||
rows = (
|
||||
session.execute(
|
||||
select(FeatureRunRow)
|
||||
.order_by(FeatureRunRow.started_at.desc())
|
||||
.limit(safe_limit)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"run_id": row.id,
|
||||
"status": row.status,
|
||||
"started_at": _dt_to_iso(row.started_at),
|
||||
"finished_at": _dt_to_iso(row.finished_at),
|
||||
"baseline_window_hours": row.baseline_window_hours,
|
||||
"entities_total": row.entities_total,
|
||||
"metrics_written": row.metrics_written,
|
||||
"anomalies_written": row.anomalies_written,
|
||||
"details": _load_json(row.details_json, {}),
|
||||
"error_message": row.error_message,
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def list_feature_metrics(
|
||||
self,
|
||||
*,
|
||||
limit: int = 200,
|
||||
metric_key: str | None = None,
|
||||
scope: str | None = None,
|
||||
run_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 2000))
|
||||
with self._session() as session:
|
||||
stmt = select(FeatureMetricRow).order_by(FeatureMetricRow.computed_at.desc(), FeatureMetricRow.id.desc())
|
||||
if metric_key:
|
||||
stmt = stmt.where(FeatureMetricRow.metric_key == metric_key)
|
||||
if scope:
|
||||
stmt = stmt.where(FeatureMetricRow.scope == scope)
|
||||
if run_id:
|
||||
stmt = stmt.where(FeatureMetricRow.feature_run_id == run_id)
|
||||
rows = session.execute(stmt.limit(safe_limit)).scalars().all()
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"id": row.id,
|
||||
"feature_run_id": row.feature_run_id,
|
||||
"metric_key": row.metric_key,
|
||||
"scope": row.scope,
|
||||
"scope_id": row.scope_id,
|
||||
"metric_type": row.metric_type,
|
||||
"metric_value": row.metric_value,
|
||||
"attributes": _load_json(row.attributes_json, {}),
|
||||
"computed_at": _dt_to_iso(row.computed_at),
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def list_anomaly_signals(
|
||||
self,
|
||||
*,
|
||||
limit: int = 200,
|
||||
severity: str | None = None,
|
||||
active_only: bool = True,
|
||||
run_id: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 2000))
|
||||
with self._session() as session:
|
||||
stmt = select(AnomalySignalRow).order_by(AnomalySignalRow.detected_at.desc(), AnomalySignalRow.id.desc())
|
||||
if active_only:
|
||||
stmt = stmt.where(AnomalySignalRow.is_active == 1)
|
||||
if severity:
|
||||
stmt = stmt.where(AnomalySignalRow.severity == severity)
|
||||
if run_id:
|
||||
stmt = stmt.where(AnomalySignalRow.feature_run_id == run_id)
|
||||
rows = session.execute(stmt.limit(safe_limit)).scalars().all()
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"id": row.id,
|
||||
"feature_run_id": row.feature_run_id,
|
||||
"signal_type": row.signal_type,
|
||||
"severity": row.severity,
|
||||
"scope": row.scope,
|
||||
"scope_id": row.scope_id,
|
||||
"score": row.score,
|
||||
"details": _load_json(row.details_json, {}),
|
||||
"detected_at": _dt_to_iso(row.detected_at),
|
||||
"is_active": bool(row.is_active),
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def feature_store_stats(self) -> dict[str, Any]:
|
||||
with self._session() as session:
|
||||
metrics_total = session.execute(select(func.count(FeatureMetricRow.id))).scalar_one()
|
||||
anomalies_total = session.execute(select(func.count(AnomalySignalRow.id))).scalar_one()
|
||||
active_anomalies_total = session.execute(
|
||||
select(func.count(AnomalySignalRow.id)).where(AnomalySignalRow.is_active == 1)
|
||||
).scalar_one()
|
||||
latest_feature_run = (
|
||||
session.execute(select(FeatureRunRow).order_by(FeatureRunRow.started_at.desc()).limit(1))
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
|
||||
latest_payload: dict[str, Any] | None = None
|
||||
if latest_feature_run is not None:
|
||||
latest_payload = {
|
||||
"run_id": latest_feature_run.id,
|
||||
"status": latest_feature_run.status,
|
||||
"started_at": _dt_to_iso(latest_feature_run.started_at),
|
||||
"finished_at": _dt_to_iso(latest_feature_run.finished_at),
|
||||
"metrics_written": latest_feature_run.metrics_written,
|
||||
"anomalies_written": latest_feature_run.anomalies_written,
|
||||
}
|
||||
|
||||
return {
|
||||
"metrics_total": int(metrics_total),
|
||||
"anomalies_total": int(anomalies_total),
|
||||
"active_anomalies_total": int(active_anomalies_total),
|
||||
"latest_feature_run": latest_payload,
|
||||
}
|
||||
|
||||
def latest_successful_feature_run(self) -> dict[str, Any] | None:
|
||||
with self._session() as session:
|
||||
row = (
|
||||
session.execute(
|
||||
select(FeatureRunRow)
|
||||
.where(FeatureRunRow.status == "success")
|
||||
.order_by(FeatureRunRow.finished_at.desc(), FeatureRunRow.started_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
return {
|
||||
"run_id": row.id,
|
||||
"status": row.status,
|
||||
"started_at": _dt_to_iso(row.started_at),
|
||||
"finished_at": _dt_to_iso(row.finished_at),
|
||||
"metrics_written": row.metrics_written,
|
||||
"anomalies_written": row.anomalies_written,
|
||||
}
|
||||
|
||||
def latest_refresh_finished_at(self) -> datetime | None:
|
||||
with self._session() as session:
|
||||
row = (
|
||||
session.execute(
|
||||
select(RefreshRunRow)
|
||||
.where(RefreshRunRow.status.in_(("success", "partial_success")))
|
||||
.order_by(RefreshRunRow.finished_at.desc())
|
||||
.limit(1)
|
||||
)
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
if row is None:
|
||||
return None
|
||||
return row.finished_at
|
||||
|
||||
def iter_entities_for_features(self, *, limit: int = 200000) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 200000))
|
||||
with self._session() as session:
|
||||
rows = (
|
||||
session.execute(
|
||||
select(CanonicalEntityRow)
|
||||
.order_by(CanonicalEntityRow.updated_at.desc())
|
||||
.limit(safe_limit)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"source_entity": row.source_entity,
|
||||
"source_id": row.source_id,
|
||||
"display_name": row.display_name,
|
||||
"attributes": _load_json(row.attributes_json, {}),
|
||||
"updated_at": row.updated_at,
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def link_counts_by_source(self) -> dict[tuple[str, str], int]:
|
||||
with self._session() as session:
|
||||
rows = session.execute(
|
||||
select(
|
||||
CanonicalLinkRow.source_entity,
|
||||
CanonicalLinkRow.source_id,
|
||||
func.count(CanonicalLinkRow.id),
|
||||
)
|
||||
.group_by(CanonicalLinkRow.source_entity, CanonicalLinkRow.source_id)
|
||||
).all()
|
||||
|
||||
output: dict[tuple[str, str], int] = {}
|
||||
for source_entity, source_id, count in rows:
|
||||
output[(str(source_entity), str(source_id))] = int(count)
|
||||
return output
|
||||
|
||||
def start_risk_run(self, *, source_feature_run_id: str | None) -> str:
|
||||
run_id = uuid4().hex
|
||||
with self._session() as session, session.begin():
|
||||
session.add(
|
||||
RiskRunRow(
|
||||
id=run_id,
|
||||
status="running",
|
||||
started_at=_utc_now(),
|
||||
source_feature_run_id=source_feature_run_id,
|
||||
)
|
||||
)
|
||||
return run_id
|
||||
|
||||
def replace_risk_patterns(self, *, run_id: str, patterns: list[dict[str, Any]]) -> int:
|
||||
now = _utc_now()
|
||||
with self._session() as session, session.begin():
|
||||
session.execute(delete(RiskPatternRow).where(RiskPatternRow.risk_run_id == run_id))
|
||||
|
||||
previous_active = session.execute(
|
||||
select(RiskPatternRow).where(RiskPatternRow.is_active == 1)
|
||||
).scalars().all()
|
||||
for item in previous_active:
|
||||
item.is_active = 0
|
||||
|
||||
for pattern in patterns:
|
||||
session.add(
|
||||
RiskPatternRow(
|
||||
risk_run_id=run_id,
|
||||
pattern_key=str(pattern.get("pattern_key", "unknown_pattern")),
|
||||
severity=str(pattern.get("severity", "low")),
|
||||
scope=str(pattern.get("scope", "global")),
|
||||
scope_id=str(pattern.get("scope_id", "")),
|
||||
score=float(pattern.get("score", 0.0)),
|
||||
confidence=float(pattern.get("confidence", 0.0)),
|
||||
details_json=_dump_json(pattern.get("details", {})),
|
||||
detected_at=now,
|
||||
is_active=1,
|
||||
)
|
||||
)
|
||||
return len(patterns)
|
||||
|
||||
def finish_risk_run(
|
||||
self,
|
||||
*,
|
||||
run_id: str,
|
||||
status: str,
|
||||
patterns_written: int,
|
||||
global_score: float,
|
||||
details: dict[str, Any] | None = None,
|
||||
error_message: str | None = None,
|
||||
) -> None:
|
||||
with self._session() as session, session.begin():
|
||||
row = session.get(RiskRunRow, run_id)
|
||||
if row is None:
|
||||
return
|
||||
row.status = status
|
||||
row.patterns_written = patterns_written
|
||||
row.global_score = global_score
|
||||
row.details_json = _dump_json(details or {})
|
||||
row.error_message = error_message
|
||||
row.finished_at = _utc_now()
|
||||
|
||||
def list_recent_risk_runs(self, limit: int = 20) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 200))
|
||||
with self._session() as session:
|
||||
rows = (
|
||||
session.execute(
|
||||
select(RiskRunRow)
|
||||
.order_by(RiskRunRow.started_at.desc())
|
||||
.limit(safe_limit)
|
||||
)
|
||||
.scalars()
|
||||
.all()
|
||||
)
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"run_id": row.id,
|
||||
"status": row.status,
|
||||
"started_at": _dt_to_iso(row.started_at),
|
||||
"finished_at": _dt_to_iso(row.finished_at),
|
||||
"source_feature_run_id": row.source_feature_run_id,
|
||||
"patterns_written": row.patterns_written,
|
||||
"global_score": row.global_score,
|
||||
"details": _load_json(row.details_json, {}),
|
||||
"error_message": row.error_message,
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def list_risk_patterns(
|
||||
self,
|
||||
*,
|
||||
limit: int = 200,
|
||||
severity: str | None = None,
|
||||
active_only: bool = True,
|
||||
run_id: str | None = None,
|
||||
pattern_key: str | None = None,
|
||||
scope: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
safe_limit = max(1, min(limit, 2000))
|
||||
with self._session() as session:
|
||||
stmt = select(RiskPatternRow).order_by(RiskPatternRow.detected_at.desc(), RiskPatternRow.id.desc())
|
||||
if active_only:
|
||||
stmt = stmt.where(RiskPatternRow.is_active == 1)
|
||||
if severity:
|
||||
stmt = stmt.where(RiskPatternRow.severity == severity)
|
||||
if run_id:
|
||||
stmt = stmt.where(RiskPatternRow.risk_run_id == run_id)
|
||||
if pattern_key:
|
||||
stmt = stmt.where(RiskPatternRow.pattern_key == pattern_key)
|
||||
if scope:
|
||||
stmt = stmt.where(RiskPatternRow.scope == scope)
|
||||
rows = session.execute(stmt.limit(safe_limit)).scalars().all()
|
||||
|
||||
output: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
output.append(
|
||||
{
|
||||
"id": row.id,
|
||||
"risk_run_id": row.risk_run_id,
|
||||
"pattern_key": row.pattern_key,
|
||||
"severity": row.severity,
|
||||
"scope": row.scope,
|
||||
"scope_id": row.scope_id,
|
||||
"score": row.score,
|
||||
"confidence": row.confidence,
|
||||
"details": _load_json(row.details_json, {}),
|
||||
"detected_at": _dt_to_iso(row.detected_at),
|
||||
"is_active": bool(row.is_active),
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def risk_store_stats(self) -> dict[str, Any]:
|
||||
with self._session() as session:
|
||||
patterns_total = session.execute(select(func.count(RiskPatternRow.id))).scalar_one()
|
||||
active_patterns_total = session.execute(
|
||||
select(func.count(RiskPatternRow.id)).where(RiskPatternRow.is_active == 1)
|
||||
).scalar_one()
|
||||
latest_run = (
|
||||
session.execute(select(RiskRunRow).order_by(RiskRunRow.started_at.desc()).limit(1))
|
||||
.scalars()
|
||||
.first()
|
||||
)
|
||||
|
||||
latest_payload: dict[str, Any] | None = None
|
||||
if latest_run is not None:
|
||||
latest_payload = {
|
||||
"run_id": latest_run.id,
|
||||
"status": latest_run.status,
|
||||
"started_at": _dt_to_iso(latest_run.started_at),
|
||||
"finished_at": _dt_to_iso(latest_run.finished_at),
|
||||
"patterns_written": latest_run.patterns_written,
|
||||
"global_score": latest_run.global_score,
|
||||
}
|
||||
|
||||
return {
|
||||
"patterns_total": int(patterns_total),
|
||||
"active_patterns_total": int(active_patterns_total),
|
||||
"latest_risk_run": latest_payload,
|
||||
}
|
||||
Reference in New Issue
Block a user