262 lines
7.7 KiB
Python
262 lines
7.7 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
from fastapi import FastAPI, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
from canonical_layer.features import FeatureService
|
|
from canonical_layer.refresh import REFRESH_MODES, RefreshService
|
|
from canonical_layer.risk import RiskService
|
|
from canonical_layer.service import CanonicalService
|
|
|
|
|
|
app = FastAPI(
|
|
title="1C Canonical Layer MVP",
|
|
version="0.1.0",
|
|
description="Read-only API over 1C OData probe and canonical mapping layer.",
|
|
)
|
|
service = CanonicalService.build()
|
|
refresh_service = RefreshService.build()
|
|
feature_service = FeatureService.build()
|
|
risk_service = RiskService.build()
|
|
|
|
|
|
class RefreshRunRequest(BaseModel):
|
|
mode: str = Field(default="incremental")
|
|
date_from: str | None = None
|
|
date_to: str | None = None
|
|
target_id: str | None = None
|
|
limit_per_set: int | None = Field(default=None, ge=1, le=5000)
|
|
entity_sets: list[str] | None = None
|
|
keywords: list[str] | None = None
|
|
|
|
|
|
class FeatureRunRequest(BaseModel):
|
|
baseline_window_hours: int | None = Field(default=None, ge=1, le=720)
|
|
stale_refresh_threshold_hours: int | None = Field(default=None, ge=1, le=720)
|
|
top_account_tokens: int = Field(default=20, ge=1, le=200)
|
|
entity_limit: int | None = Field(default=None, ge=1, le=200000)
|
|
|
|
|
|
class RiskRunRequest(BaseModel):
|
|
source_feature_run_id: str | None = None
|
|
anomaly_limit: int | None = Field(default=None, ge=1, le=20000)
|
|
|
|
|
|
@app.get("/health")
|
|
def health() -> dict[str, str]:
|
|
return {
|
|
"status": "ok",
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
|
|
|
|
@app.get("/metadata/entity-sets")
|
|
def metadata_entity_sets() -> dict[str, Any]:
|
|
return service.list_entity_sets()
|
|
|
|
|
|
@app.get("/documents")
|
|
def list_documents(
|
|
from_date: str | None = Query(default=None, alias="from"),
|
|
to_date: str | None = Query(default=None, alias="to"),
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
) -> dict[str, Any]:
|
|
items = service.get_documents(date_from=from_date, date_to=to_date, limit=limit)
|
|
return {
|
|
"total": len(items),
|
|
"items": [item.model_dump() for item in items],
|
|
}
|
|
|
|
|
|
@app.get("/documents/{document_id}")
|
|
def get_document(document_id: str) -> dict[str, Any]:
|
|
document = service.get_document(document_id)
|
|
if document is None:
|
|
raise HTTPException(status_code=404, detail="Document not found in current probe window")
|
|
return document.model_dump()
|
|
|
|
|
|
@app.get("/postings")
|
|
def list_postings(
|
|
account: str | None = Query(default=None),
|
|
from_date: str | None = Query(default=None, alias="from"),
|
|
to_date: str | None = Query(default=None, alias="to"),
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
) -> dict[str, Any]:
|
|
items = service.get_postings(account=account, date_from=from_date, date_to=to_date, limit=limit)
|
|
return {
|
|
"total": len(items),
|
|
"items": [item.model_dump() for item in items],
|
|
}
|
|
|
|
|
|
@app.get("/counterparties/{counterparty_id}/documents")
|
|
def counterparty_documents(
|
|
counterparty_id: str,
|
|
limit: int = Query(default=100, ge=1, le=500),
|
|
) -> dict[str, Any]:
|
|
items = service.get_counterparty_documents(counterparty_id=counterparty_id, limit=limit)
|
|
return {
|
|
"counterparty_id": counterparty_id,
|
|
"total": len(items),
|
|
"items": [item.model_dump() for item in items],
|
|
}
|
|
|
|
|
|
@app.get("/graph/document/{document_id}")
|
|
def document_graph(document_id: str) -> dict[str, Any]:
|
|
return service.build_document_graph(document_id)
|
|
|
|
|
|
@app.get("/store/stats")
|
|
def store_stats() -> dict[str, Any]:
|
|
return refresh_service.store_stats()
|
|
|
|
|
|
@app.get("/refresh/runs")
|
|
def refresh_runs(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
|
|
runs = refresh_service.list_recent_runs(limit=limit)
|
|
return {
|
|
"total": len(runs),
|
|
"items": runs,
|
|
}
|
|
|
|
|
|
@app.post("/refresh/run")
|
|
def run_refresh(request: RefreshRunRequest) -> dict[str, Any]:
|
|
mode = request.mode.strip().lower()
|
|
if mode not in REFRESH_MODES:
|
|
raise HTTPException(status_code=400, detail=f"Unsupported mode '{request.mode}'")
|
|
|
|
result = refresh_service.run_refresh(
|
|
mode=mode,
|
|
date_from=request.date_from,
|
|
date_to=request.date_to,
|
|
target_id=request.target_id,
|
|
limit_per_set=request.limit_per_set,
|
|
requested_entity_sets=request.entity_sets,
|
|
entity_keywords=request.keywords,
|
|
)
|
|
payload = result.to_dict()
|
|
payload["store_stats"] = refresh_service.store_stats()
|
|
return payload
|
|
|
|
|
|
@app.get("/features/stats")
|
|
def feature_stats() -> dict[str, Any]:
|
|
return feature_service.stats()
|
|
|
|
|
|
@app.get("/features/runs")
|
|
def feature_runs(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
|
|
runs = feature_service.list_recent_runs(limit=limit)
|
|
return {
|
|
"total": len(runs),
|
|
"items": runs,
|
|
}
|
|
|
|
|
|
@app.get("/features/metrics")
|
|
def feature_metrics(
|
|
limit: int = Query(default=200, ge=1, le=2000),
|
|
metric_key: str | None = Query(default=None),
|
|
scope: str | None = Query(default=None),
|
|
run_id: str | None = Query(default=None),
|
|
) -> dict[str, Any]:
|
|
items = feature_service.list_metrics(
|
|
limit=limit,
|
|
metric_key=metric_key,
|
|
scope=scope,
|
|
run_id=run_id,
|
|
)
|
|
return {
|
|
"total": len(items),
|
|
"items": items,
|
|
}
|
|
|
|
|
|
@app.get("/features/anomalies")
|
|
def feature_anomalies(
|
|
limit: int = Query(default=200, ge=1, le=2000),
|
|
severity: str | None = Query(default=None),
|
|
active_only: bool = Query(default=True),
|
|
run_id: str | None = Query(default=None),
|
|
) -> dict[str, Any]:
|
|
items = feature_service.list_anomalies(
|
|
limit=limit,
|
|
severity=severity,
|
|
active_only=active_only,
|
|
run_id=run_id,
|
|
)
|
|
return {
|
|
"total": len(items),
|
|
"items": items,
|
|
}
|
|
|
|
|
|
@app.post("/features/run")
|
|
def run_features(request: FeatureRunRequest) -> dict[str, Any]:
|
|
result = feature_service.run_feature_engine(
|
|
baseline_window_hours=request.baseline_window_hours,
|
|
stale_refresh_threshold_hours=request.stale_refresh_threshold_hours,
|
|
top_account_tokens=request.top_account_tokens,
|
|
entity_limit=request.entity_limit,
|
|
)
|
|
payload = result.to_dict()
|
|
payload["feature_store_stats"] = feature_service.stats()
|
|
payload["store_stats"] = refresh_service.store_stats()
|
|
return payload
|
|
|
|
|
|
@app.get("/risk/stats")
|
|
def risk_stats() -> dict[str, Any]:
|
|
return risk_service.stats()
|
|
|
|
|
|
@app.get("/risk/runs")
|
|
def risk_runs(limit: int = Query(default=20, ge=1, le=200)) -> dict[str, Any]:
|
|
runs = risk_service.list_recent_runs(limit=limit)
|
|
return {
|
|
"total": len(runs),
|
|
"items": runs,
|
|
}
|
|
|
|
|
|
@app.get("/risk/patterns")
|
|
def risk_patterns(
|
|
limit: int = Query(default=200, ge=1, le=2000),
|
|
severity: str | None = Query(default=None),
|
|
active_only: bool = Query(default=True),
|
|
run_id: str | None = Query(default=None),
|
|
pattern_key: str | None = Query(default=None),
|
|
scope: str | None = Query(default=None),
|
|
) -> dict[str, Any]:
|
|
items = risk_service.list_patterns(
|
|
limit=limit,
|
|
severity=severity,
|
|
active_only=active_only,
|
|
run_id=run_id,
|
|
pattern_key=pattern_key,
|
|
scope=scope,
|
|
)
|
|
return {
|
|
"total": len(items),
|
|
"items": items,
|
|
}
|
|
|
|
|
|
@app.post("/risk/run")
|
|
def run_risk(request: RiskRunRequest) -> dict[str, Any]:
|
|
result = risk_service.run_risk_engine(
|
|
source_feature_run_id=request.source_feature_run_id,
|
|
anomaly_limit=request.anomaly_limit,
|
|
)
|
|
payload = result.to_dict()
|
|
payload["risk_store_stats"] = risk_service.stats()
|
|
payload["feature_store_stats"] = feature_service.stats()
|
|
payload["store_stats"] = refresh_service.store_stats()
|
|
return payload
|