177 lines
6.3 KiB
Python
177 lines
6.3 KiB
Python
import os
|
|
import json
|
|
import yaml
|
|
from datetime import datetime
|
|
from fastapi import APIRouter, status
|
|
from app.core.config import settings
|
|
from app.schemas.sitomat import (
|
|
BundleItem,
|
|
BundleListResponse,
|
|
SitomatProvisionRequest,
|
|
SitomatProvisionResponse,
|
|
SitomatTriggerRequest,
|
|
SitomatTriggerResponse,
|
|
SitomatLedgerCallback,
|
|
)
|
|
|
|
router = APIRouter(prefix="/sitomat", tags=["sitomat Ingestion Governance"])
|
|
|
|
@router.get("/health")
|
|
def sitomat_health():
|
|
"""Health status of sitomat control plane integration bridge."""
|
|
return {
|
|
"status": "online",
|
|
"service": "MERICA sitomat Governance Bridge",
|
|
"supported_bundles": ["dlh-core-ingestion", "bpkp-perizinan"]
|
|
}
|
|
|
|
@router.get("/bundles", response_model=BundleListResponse)
|
|
def list_dag_bundles():
|
|
"""
|
|
List configured Airflow 3 GitDagBundles available for pipeline binding.
|
|
Matches AIRFLOW__DAG_PROCESSOR__DAG_BUNDLE_CONFIG_LIST specifications.
|
|
"""
|
|
return {
|
|
"bundles": [
|
|
{
|
|
"name": "dlh-core-ingestion",
|
|
"tracking_ref": "development",
|
|
"repo_url": "https://repo.sicabe.com/sekota/merica-beta.git",
|
|
"subdir": "airflow/configs",
|
|
"type": "git"
|
|
},
|
|
{
|
|
"name": "bpkp-perizinan",
|
|
"tracking_ref": "development",
|
|
"repo_url": "https://repo.sicabe.com/sekota/merica-beta.git",
|
|
"subdir": "airflow/configs",
|
|
"type": "git"
|
|
}
|
|
]
|
|
}
|
|
|
|
@router.post("/pipelines/provision", response_model=SitomatProvisionResponse, status_code=status.HTTP_201_CREATED)
|
|
def provision_sitomat_pipeline(req: SitomatProvisionRequest):
|
|
"""
|
|
Control Plane Handshake (Gate-1):
|
|
Register metadata, compile initial YAML config for Airflow Git DAG Bundle,
|
|
and create initial semantic version snapshot (v1.0.0).
|
|
"""
|
|
clean_id = req.dataset_id.lower().replace(" ", "_")
|
|
bronze_dag_id = f"ingestion_{clean_id}"
|
|
transform_dag_id = f"transform_{clean_id}"
|
|
bundle_name = req.bundle_name or "dlh-core-ingestion"
|
|
initial_version = "v1.0.0"
|
|
now_iso = datetime.utcnow().isoformat() + "Z"
|
|
|
|
# 1. Canonical YAML specification for Airflow DAG builder
|
|
yaml_config = {
|
|
"pipeline_id": bronze_dag_id,
|
|
"dataset_id": req.dataset_id,
|
|
"bundle_name": bundle_name,
|
|
"version": initial_version,
|
|
"schedule": req.schedule or "0 2 * * *",
|
|
"mode": "medallion",
|
|
"bronze": {
|
|
"source_type": req.source_type,
|
|
"connection_id": req.connection_id,
|
|
"source_config": req.source_config,
|
|
"target": req.bronze_target or {
|
|
"s3_bucket": "dlh-landing",
|
|
"s3_prefix": f"bronze/raw/{clean_id}/"
|
|
}
|
|
},
|
|
"silver": {
|
|
"transformation": {
|
|
"select_models": req.dbt_models.get("silver", f"stg_{clean_id}") if req.dbt_models else f"stg_{clean_id}"
|
|
}
|
|
},
|
|
"gold": {
|
|
"transformation": {
|
|
"select_models": req.dbt_models.get("gold", f"fct_{clean_id}") if req.dbt_models else f"fct_{clean_id}"
|
|
}
|
|
}
|
|
}
|
|
|
|
os.makedirs(settings.CONFIGS_DIR, exist_ok=True)
|
|
target_meta_dir = getattr(settings, "METADATA_DIR", settings.CONFIGS_DIR)
|
|
os.makedirs(target_meta_dir, exist_ok=True)
|
|
|
|
yaml_filename = f"{bronze_dag_id}.yaml"
|
|
yaml_path = os.path.join(settings.CONFIGS_DIR, yaml_filename)
|
|
yaml_header = (
|
|
f"# ============================================================\n"
|
|
f"# MERICA Studio Data Lakehouse Pipeline\n"
|
|
f"# Airflow 3 DAG Bundle: {bundle_name} | Dataset: {req.dataset_id}\n"
|
|
f"# Initial Version: {initial_version} | Provisioned: {now_iso}\n"
|
|
f"# ============================================================\n"
|
|
)
|
|
with open(yaml_path, "w", encoding="utf-8") as f:
|
|
f.write(yaml_header + yaml.dump(yaml_config, sort_keys=False))
|
|
|
|
# 2. JSON metadata for UI Canvas load
|
|
json_path = os.path.join(target_meta_dir, f"{bronze_dag_id}.json")
|
|
canvas_metadata = {
|
|
"id": bronze_dag_id,
|
|
"dataset_id": req.dataset_id,
|
|
"bundle_name": bundle_name,
|
|
"version": initial_version,
|
|
"changelog": "Initial provisioning via sitomat Control Plane",
|
|
"published_at": now_iso,
|
|
"is_draft": False,
|
|
"status": "deployed",
|
|
"nodes": [],
|
|
"edges": []
|
|
}
|
|
with open(json_path, "w", encoding="utf-8") as f:
|
|
json.dump(canvas_metadata, f, indent=2)
|
|
|
|
# 3. Create initial version snapshot file for Time-Machine
|
|
snapshot_filename = f"{bronze_dag_id}.version.{initial_version}.json"
|
|
snapshot_path = os.path.join(target_meta_dir, snapshot_filename)
|
|
with open(snapshot_path, "w", encoding="utf-8") as f:
|
|
json.dump(canvas_metadata, f, indent=2)
|
|
|
|
return SitomatProvisionResponse(
|
|
status="provisioned",
|
|
dataset_id=req.dataset_id,
|
|
bundle_name=bundle_name,
|
|
bronze_dag_id=bronze_dag_id,
|
|
transform_dag_id=transform_dag_id,
|
|
initial_version=initial_version,
|
|
git_commit_hash="local-dev",
|
|
yaml_config_path=yaml_path
|
|
)
|
|
|
|
@router.post("/runs/trigger", response_model=SitomatTriggerResponse)
|
|
def trigger_sitomat_run(req: SitomatTriggerRequest):
|
|
"""
|
|
Data Plane Trigger:
|
|
sitomat triggers an ingestion execution with correlated ledger run ID and pinned version.
|
|
"""
|
|
clean_id = req.dataset_id.lower().replace(" ", "_")
|
|
dag_id = f"ingestion_{clean_id}"
|
|
dag_run_id = f"sitomat_run__{req.correlation_id}"
|
|
pinned_version = req.target_dag_version or "latest"
|
|
|
|
return SitomatTriggerResponse(
|
|
status="triggered",
|
|
correlation_id=req.correlation_id,
|
|
dag_id=dag_id,
|
|
dag_run_id=dag_run_id,
|
|
pinned_version=pinned_version
|
|
)
|
|
|
|
@router.post("/runs/callback")
|
|
def sitomat_run_callback(payload: SitomatLedgerCallback):
|
|
"""
|
|
Ingestion Run Ledger Callback:
|
|
Airflow task notifies sitomat Control Plane when Bronze extraction completes.
|
|
"""
|
|
return {
|
|
"status": "received",
|
|
"correlation_id": payload.correlation_id,
|
|
"dataset_id": payload.dataset_id,
|
|
"message": f"Ledger updated for run '{payload.dag_run_id}' with status '{payload.status}'."
|
|
}
|