714 lines
24 KiB
Python
714 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generic Pipeline API endpoints.
|
|
|
|
Provides a unified API for all registered pipelines:
|
|
- List available pipelines
|
|
- Get pipeline details and metadata
|
|
- Execute pipelines
|
|
- Get dashboard configuration
|
|
- Get widget data
|
|
- List execution history
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import asyncpg
|
|
from fastapi import APIRouter, HTTPException, Query
|
|
from pydantic import BaseModel, Field
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# Create router
|
|
router = APIRouter(prefix="/api/pipelines", tags=["pipelines"])
|
|
|
|
# Database pool (set by main server)
|
|
_pool: asyncpg.Pool | None = None
|
|
|
|
# Pipeline instances cache
|
|
_pipeline_instances: dict[str, Any] = {}
|
|
|
|
|
|
def set_database(pool: asyncpg.Pool) -> None:
|
|
"""Set the database pool for pipeline operations."""
|
|
global _pool
|
|
_pool = pool
|
|
|
|
|
|
# ==================== Pydantic Models ====================
|
|
|
|
|
|
class PipelineInfo(BaseModel):
|
|
"""Summary information about a pipeline."""
|
|
|
|
id: str = Field(..., description="Pipeline identifier")
|
|
name: str = Field(..., description="Display name")
|
|
description: str = Field(..., description="Human-readable description")
|
|
version: str = Field(..., description="Semantic version")
|
|
is_enabled: bool = Field(..., description="Whether pipeline is enabled")
|
|
stages: list[str] = Field(..., description="Available stages")
|
|
input_type: str = Field(..., description="Expected input type")
|
|
|
|
|
|
class PipelineDetail(PipelineInfo):
|
|
"""Detailed pipeline information."""
|
|
|
|
module_path: str = Field(..., description="Python module path")
|
|
config: dict[str, Any] | None = Field(None, description="Pipeline configuration")
|
|
created_at: str | None = Field(None, description="Registration timestamp")
|
|
updated_at: str | None = Field(None, description="Last update timestamp")
|
|
|
|
|
|
class ExecuteRequest(BaseModel):
|
|
"""Request to execute a pipeline."""
|
|
|
|
job_id: str | None = Field(None, description="Job ID to process")
|
|
business_id: str | None = Field(None, description="Business identifier")
|
|
input_data: dict[str, Any] | None = Field(None, description="Direct input data")
|
|
stages: list[str] | None = Field(None, description="Stages to run (default: all)")
|
|
options: dict[str, Any] | None = Field(None, description="Pipeline-specific options")
|
|
|
|
|
|
class ExecuteResponse(BaseModel):
|
|
"""Response from pipeline execution."""
|
|
|
|
execution_id: str = Field(..., description="Execution identifier")
|
|
pipeline_id: str = Field(..., description="Pipeline that was executed")
|
|
success: bool = Field(..., description="Whether execution succeeded")
|
|
stages_run: list[str] = Field(..., description="Stages that were run")
|
|
error: str | None = Field(None, description="Error message if failed")
|
|
|
|
|
|
class ExecutionSummary(BaseModel):
|
|
"""Summary of a pipeline execution."""
|
|
|
|
id: str = Field(..., description="Execution identifier")
|
|
pipeline_id: str = Field(..., description="Pipeline identifier")
|
|
job_id: str | None = Field(None, description="Associated job ID")
|
|
business_id: str | None = Field(None, description="Business identifier")
|
|
status: str = Field(..., description="Execution status")
|
|
stages_requested: list[str] = Field(..., description="Stages requested")
|
|
stages_completed: list[str] = Field(..., description="Stages completed")
|
|
error_message: str | None = Field(None, description="Error if failed")
|
|
started_at: str | None = Field(None, description="Start timestamp")
|
|
completed_at: str | None = Field(None, description="Completion timestamp")
|
|
created_at: str | None = Field(None, description="Creation timestamp")
|
|
|
|
|
|
class DashboardSectionModel(BaseModel):
|
|
"""Dashboard section configuration."""
|
|
|
|
id: str
|
|
title: str
|
|
description: str | None = None
|
|
widgets: list[dict[str, Any]]
|
|
collapsed: bool | None = None
|
|
|
|
|
|
class DashboardConfigModel(BaseModel):
|
|
"""Dashboard configuration for a pipeline."""
|
|
|
|
pipeline_id: str
|
|
title: str
|
|
description: str | None = None
|
|
sections: list[DashboardSectionModel]
|
|
default_time_range: str | None = None
|
|
refresh_interval: int | None = None
|
|
|
|
|
|
# ==================== Helper Functions ====================
|
|
|
|
|
|
async def _get_pipeline_instance(pipeline_id: str) -> Any:
|
|
"""Get or create a pipeline instance."""
|
|
if pipeline_id in _pipeline_instances:
|
|
return _pipeline_instances[pipeline_id]
|
|
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
# Look up pipeline in registry
|
|
async with _pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
SELECT pipeline_id, module_path, is_enabled
|
|
FROM pipeline.registry
|
|
WHERE pipeline_id = $1
|
|
""",
|
|
pipeline_id,
|
|
)
|
|
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"Pipeline not found: {pipeline_id}")
|
|
|
|
if not row["is_enabled"]:
|
|
raise HTTPException(status_code=400, detail=f"Pipeline is disabled: {pipeline_id}")
|
|
|
|
# Import and instantiate
|
|
try:
|
|
module_path = row["module_path"]
|
|
module_name, class_name = module_path.rsplit(":", 1)
|
|
|
|
import importlib
|
|
|
|
module = importlib.import_module(module_name)
|
|
cls = getattr(module, class_name)
|
|
instance = cls()
|
|
|
|
# Initialize the pipeline
|
|
await instance.initialize()
|
|
|
|
_pipeline_instances[pipeline_id] = instance
|
|
return instance
|
|
|
|
except Exception as e:
|
|
log.exception(f"Failed to load pipeline {pipeline_id}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to load pipeline: {e}"
|
|
)
|
|
|
|
|
|
# ==================== API Endpoints ====================
|
|
|
|
|
|
@router.get("/", response_model=list[PipelineInfo])
|
|
async def list_pipelines(
|
|
enabled_only: bool = Query(True, description="Only return enabled pipelines"),
|
|
) -> list[PipelineInfo]:
|
|
"""
|
|
List all registered pipelines.
|
|
|
|
Returns summary information for each pipeline including name, version,
|
|
available stages, and enabled status.
|
|
"""
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
async with _pool.acquire() as conn:
|
|
if enabled_only:
|
|
rows = await conn.fetch(
|
|
"""
|
|
SELECT pipeline_id, name, description, version,
|
|
is_enabled, stages, input_type
|
|
FROM pipeline.registry
|
|
WHERE is_enabled = TRUE
|
|
ORDER BY name
|
|
"""
|
|
)
|
|
else:
|
|
rows = await conn.fetch(
|
|
"""
|
|
SELECT pipeline_id, name, description, version,
|
|
is_enabled, stages, input_type
|
|
FROM pipeline.registry
|
|
ORDER BY name
|
|
"""
|
|
)
|
|
|
|
return [
|
|
PipelineInfo(
|
|
id=row["pipeline_id"],
|
|
name=row["name"],
|
|
description=row["description"] or "",
|
|
version=row["version"],
|
|
is_enabled=row["is_enabled"],
|
|
stages=row["stages"] or [],
|
|
input_type=row["input_type"] or "dict",
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
|
|
@router.get("/{pipeline_id}", response_model=PipelineDetail)
|
|
async def get_pipeline(pipeline_id: str) -> PipelineDetail:
|
|
"""
|
|
Get detailed information about a pipeline.
|
|
|
|
Includes metadata, configuration, and registration timestamps.
|
|
"""
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
async with _pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
SELECT pipeline_id, name, description, version, module_path,
|
|
is_enabled, stages, input_type, config,
|
|
created_at, updated_at
|
|
FROM pipeline.registry
|
|
WHERE pipeline_id = $1
|
|
""",
|
|
pipeline_id,
|
|
)
|
|
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"Pipeline not found: {pipeline_id}")
|
|
|
|
return PipelineDetail(
|
|
id=row["pipeline_id"],
|
|
name=row["name"],
|
|
description=row["description"] or "",
|
|
version=row["version"],
|
|
is_enabled=row["is_enabled"],
|
|
stages=row["stages"] or [],
|
|
input_type=row["input_type"] or "dict",
|
|
module_path=row["module_path"],
|
|
config=row["config"],
|
|
created_at=row["created_at"].isoformat() if row["created_at"] else None,
|
|
updated_at=row["updated_at"].isoformat() if row["updated_at"] else None,
|
|
)
|
|
|
|
|
|
@router.post("/{pipeline_id}/execute", response_model=ExecuteResponse)
|
|
async def execute_pipeline(
|
|
pipeline_id: str,
|
|
request: ExecuteRequest,
|
|
) -> ExecuteResponse:
|
|
"""
|
|
Execute a pipeline.
|
|
|
|
The pipeline can be executed with:
|
|
- A job_id to process an existing scraper job
|
|
- Direct input_data for testing
|
|
- Specific stages to run (default: all)
|
|
"""
|
|
import uuid
|
|
|
|
pipeline = await _get_pipeline_instance(pipeline_id)
|
|
|
|
# Create execution record
|
|
execution_id = str(uuid.uuid4())
|
|
|
|
# Prepare input data
|
|
input_data = request.input_data or {}
|
|
if request.job_id:
|
|
input_data["job_id"] = request.job_id
|
|
if request.business_id:
|
|
input_data["business_id"] = request.business_id
|
|
# Pass execution_id so Stage 5 synthesis can store results
|
|
input_data["execution_id"] = execution_id
|
|
stages = request.stages or pipeline.get_stage_names()
|
|
|
|
# Prepare input summary for storage
|
|
import json
|
|
input_summary = {
|
|
"job_id": request.job_id,
|
|
"business_id": request.business_id,
|
|
"stages": stages,
|
|
}
|
|
|
|
if _pool:
|
|
async with _pool.acquire() as conn:
|
|
await conn.execute(
|
|
"""
|
|
INSERT INTO pipeline.executions (
|
|
id, pipeline_id, job_id, business_id,
|
|
status, stages_requested, input_summary,
|
|
started_at, created_at
|
|
)
|
|
VALUES ($1, $2, $3, $4, 'running', $5, $6, NOW(), NOW())
|
|
""",
|
|
uuid.UUID(execution_id),
|
|
pipeline_id,
|
|
uuid.UUID(request.job_id) if request.job_id else None,
|
|
request.business_id,
|
|
stages,
|
|
json.dumps(input_summary),
|
|
)
|
|
|
|
try:
|
|
import time
|
|
start_time = time.time()
|
|
|
|
# Execute pipeline
|
|
result = await pipeline.process(input_data, stages=stages)
|
|
|
|
# Calculate total duration
|
|
total_duration_ms = int((time.time() - start_time) * 1000)
|
|
|
|
# Prepare result summary
|
|
result_summary = {
|
|
"success": result.success,
|
|
"stages_run": result.stages_run,
|
|
}
|
|
if hasattr(result, "summary") and result.summary:
|
|
result_summary.update(result.summary)
|
|
|
|
# Extract stage metrics for profiling
|
|
stage_metrics = {}
|
|
if hasattr(result, "stage_results") and result.stage_results:
|
|
for stage_name, stage_result in result.stage_results.items():
|
|
stage_metrics[stage_name] = {
|
|
"duration_ms": stage_result.get("duration_ms", 0),
|
|
"success": stage_result.get("success", True),
|
|
"records_in": stage_result.get("records_in", 0),
|
|
"records_out": stage_result.get("records_out", 0),
|
|
"error": stage_result.get("error"),
|
|
}
|
|
|
|
# Update execution status with metrics
|
|
if _pool:
|
|
async with _pool.acquire() as conn:
|
|
await conn.execute(
|
|
"""
|
|
UPDATE pipeline.executions
|
|
SET status = $2, stages_completed = $3, error_message = $4,
|
|
result_summary = $5, stage_metrics = $6,
|
|
total_duration_ms = $7, completed_at = NOW()
|
|
WHERE id = $1
|
|
""",
|
|
uuid.UUID(execution_id),
|
|
"completed" if result.success else "failed",
|
|
result.stages_run,
|
|
result.error,
|
|
json.dumps(result_summary),
|
|
json.dumps(stage_metrics) if stage_metrics else None,
|
|
total_duration_ms,
|
|
)
|
|
|
|
return ExecuteResponse(
|
|
execution_id=execution_id,
|
|
pipeline_id=pipeline_id,
|
|
success=result.success,
|
|
stages_run=result.stages_run,
|
|
error=result.error,
|
|
)
|
|
|
|
except Exception as e:
|
|
log.exception(f"Pipeline execution failed: {e}")
|
|
|
|
# Update execution status
|
|
if _pool:
|
|
async with _pool.acquire() as conn:
|
|
await conn.execute(
|
|
"""
|
|
UPDATE pipeline.executions
|
|
SET status = 'failed', error_message = $2, completed_at = NOW()
|
|
WHERE id = $1
|
|
""",
|
|
uuid.UUID(execution_id),
|
|
str(e),
|
|
)
|
|
|
|
raise HTTPException(status_code=500, detail=f"Execution failed: {e}")
|
|
|
|
|
|
@router.get("/{pipeline_id}/executions", response_model=list[ExecutionSummary])
|
|
async def list_executions(
|
|
pipeline_id: str,
|
|
status: str | None = Query(None, description="Filter by status"),
|
|
limit: int = Query(50, ge=1, le=200, description="Max results"),
|
|
offset: int = Query(0, ge=0, description="Offset for pagination"),
|
|
) -> list[ExecutionSummary]:
|
|
"""
|
|
List execution history for a pipeline.
|
|
|
|
Can filter by status and paginate results.
|
|
"""
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
conditions = ["pipeline_id = $1"]
|
|
params: list[Any] = [pipeline_id]
|
|
param_idx = 2
|
|
|
|
if status:
|
|
conditions.append(f"status = ${param_idx}")
|
|
params.append(status)
|
|
param_idx += 1
|
|
|
|
where_clause = " AND ".join(conditions)
|
|
|
|
async with _pool.acquire() as conn:
|
|
rows = await conn.fetch(
|
|
f"""
|
|
SELECT id, pipeline_id, job_id, business_id, status,
|
|
stages_requested, stages_completed, error_message,
|
|
started_at, completed_at, created_at
|
|
FROM pipeline.executions
|
|
WHERE {where_clause}
|
|
ORDER BY created_at DESC
|
|
LIMIT ${param_idx} OFFSET ${param_idx + 1}
|
|
""",
|
|
*params,
|
|
limit,
|
|
offset,
|
|
)
|
|
|
|
return [
|
|
ExecutionSummary(
|
|
id=str(row["id"]),
|
|
pipeline_id=row["pipeline_id"],
|
|
job_id=str(row["job_id"]) if row["job_id"] else None,
|
|
business_id=row["business_id"],
|
|
status=row["status"],
|
|
stages_requested=row["stages_requested"] or [],
|
|
stages_completed=row["stages_completed"] or [],
|
|
error_message=row["error_message"],
|
|
started_at=row["started_at"].isoformat() if row["started_at"] else None,
|
|
completed_at=row["completed_at"].isoformat() if row["completed_at"] else None,
|
|
created_at=row["created_at"].isoformat() if row["created_at"] else None,
|
|
)
|
|
for row in rows
|
|
]
|
|
|
|
|
|
class StageMetrics(BaseModel):
|
|
"""Metrics for a single pipeline stage."""
|
|
|
|
duration_ms: int = Field(0, description="Stage execution time in ms")
|
|
success: bool = Field(True, description="Whether stage succeeded")
|
|
records_in: int = Field(0, description="Records input to stage")
|
|
records_out: int = Field(0, description="Records output from stage")
|
|
error: str | None = Field(None, description="Error message if failed")
|
|
|
|
|
|
class ExecutionDetail(BaseModel):
|
|
"""Detailed execution information."""
|
|
|
|
id: str = Field(..., description="Execution identifier")
|
|
pipeline_id: str = Field(..., description="Pipeline identifier")
|
|
job_id: str | None = Field(None, description="Associated job ID")
|
|
business_id: str | None = Field(None, description="Business identifier")
|
|
status: str = Field(..., description="Execution status")
|
|
stages_requested: list[str] = Field(..., description="Stages requested")
|
|
stages_completed: list[str] = Field(..., description="Stages completed")
|
|
current_stage: str | None = Field(None, description="Currently executing stage")
|
|
progress: int = Field(0, description="Progress percentage")
|
|
error_message: str | None = Field(None, description="Error if failed")
|
|
started_at: str | None = Field(None, description="Start timestamp")
|
|
completed_at: str | None = Field(None, description="Completion timestamp")
|
|
created_at: str | None = Field(None, description="Creation timestamp")
|
|
total_duration_ms: int | None = Field(None, description="Total execution time in ms")
|
|
input_summary: dict[str, Any] | None = Field(None, description="Input data summary")
|
|
result_summary: dict[str, Any] | None = Field(None, description="Result summary")
|
|
stage_metrics: dict[str, StageMetrics] | None = Field(None, description="Per-stage execution metrics")
|
|
|
|
|
|
@router.get("/{pipeline_id}/executions/{execution_id}", response_model=ExecutionDetail)
|
|
async def get_execution(pipeline_id: str, execution_id: str) -> ExecutionDetail:
|
|
"""
|
|
Get details for a specific execution.
|
|
|
|
Returns full execution details including stage results and summaries.
|
|
"""
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
import uuid
|
|
|
|
try:
|
|
exec_uuid = uuid.UUID(execution_id)
|
|
except ValueError:
|
|
raise HTTPException(status_code=400, detail="Invalid execution ID format")
|
|
|
|
async with _pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
SELECT id, pipeline_id, job_id, business_id, status,
|
|
stages_requested, stages_completed, error_message,
|
|
input_summary, result_summary, stage_metrics,
|
|
total_duration_ms, started_at, completed_at, created_at
|
|
FROM pipeline.executions
|
|
WHERE pipeline_id = $1 AND id = $2
|
|
""",
|
|
pipeline_id,
|
|
exec_uuid,
|
|
)
|
|
|
|
if not row:
|
|
raise HTTPException(status_code=404, detail=f"Execution not found: {execution_id}")
|
|
|
|
# Calculate progress
|
|
stages_requested = row["stages_requested"] or []
|
|
stages_completed = row["stages_completed"] or []
|
|
progress = (
|
|
int(len(stages_completed) / len(stages_requested) * 100)
|
|
if stages_requested
|
|
else 0
|
|
)
|
|
|
|
# Parse JSONB fields (asyncpg may return them as strings)
|
|
import json
|
|
|
|
def parse_json_field(value):
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, str):
|
|
try:
|
|
return json.loads(value)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
return value
|
|
|
|
return ExecutionDetail(
|
|
id=str(row["id"]),
|
|
pipeline_id=row["pipeline_id"],
|
|
job_id=str(row["job_id"]) if row["job_id"] else None,
|
|
business_id=row["business_id"],
|
|
status=row["status"],
|
|
stages_requested=stages_requested,
|
|
stages_completed=stages_completed,
|
|
current_stage=None, # Could be tracked if needed
|
|
progress=progress,
|
|
error_message=row["error_message"],
|
|
started_at=row["started_at"].isoformat() if row["started_at"] else None,
|
|
completed_at=row["completed_at"].isoformat() if row["completed_at"] else None,
|
|
created_at=row["created_at"].isoformat() if row["created_at"] else None,
|
|
total_duration_ms=row["total_duration_ms"],
|
|
input_summary=parse_json_field(row["input_summary"]),
|
|
result_summary=parse_json_field(row["result_summary"]),
|
|
stage_metrics=parse_json_field(row["stage_metrics"]),
|
|
)
|
|
|
|
|
|
@router.get("/{pipeline_id}/dashboard", response_model=DashboardConfigModel)
|
|
async def get_dashboard_config(pipeline_id: str) -> DashboardConfigModel:
|
|
"""
|
|
Get dashboard configuration for a pipeline.
|
|
|
|
Returns the widget configuration that the frontend uses to render
|
|
the pipeline's dynamic dashboard.
|
|
"""
|
|
pipeline = await _get_pipeline_instance(pipeline_id)
|
|
|
|
try:
|
|
config = pipeline.get_dashboard_config()
|
|
|
|
return DashboardConfigModel(
|
|
pipeline_id=config["pipeline_id"],
|
|
title=config["title"],
|
|
description=config.get("description"),
|
|
sections=[
|
|
DashboardSectionModel(
|
|
id=s["id"],
|
|
title=s["title"],
|
|
description=s.get("description"),
|
|
widgets=s["widgets"],
|
|
collapsed=s.get("collapsed"),
|
|
)
|
|
for s in config["sections"]
|
|
],
|
|
default_time_range=config.get("default_time_range"),
|
|
refresh_interval=config.get("refresh_interval"),
|
|
)
|
|
|
|
except Exception as e:
|
|
log.exception(f"Failed to get dashboard config: {e}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to get dashboard config: {e}"
|
|
)
|
|
|
|
|
|
@router.get("/{pipeline_id}/widgets/{widget_id}")
|
|
async def get_widget_data(
|
|
pipeline_id: str,
|
|
widget_id: str,
|
|
business_id: str | None = Query(None, description="Filter by business"),
|
|
job_id: str | None = Query(None, description="Filter by job ID"),
|
|
time_range: str = Query("30d", description="Time range (e.g., 7d, 30d, 90d)"),
|
|
page: int = Query(1, ge=1, description="Page number for paginated widgets"),
|
|
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get data for a specific dashboard widget.
|
|
|
|
The response format depends on the widget type. Common formats:
|
|
- stat_card: {value, trend, ...}
|
|
- chart: {data: [{x, y, ...}, ...]}
|
|
- table: {data: [...], total: n}
|
|
"""
|
|
pipeline = await _get_pipeline_instance(pipeline_id)
|
|
|
|
try:
|
|
params = {
|
|
"business_id": business_id,
|
|
"job_id": job_id,
|
|
"time_range": time_range,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
}
|
|
|
|
data = await pipeline.get_widget_data(widget_id, params)
|
|
return data
|
|
|
|
except Exception as e:
|
|
log.exception(f"Failed to get widget data: {e}")
|
|
raise HTTPException(
|
|
status_code=500, detail=f"Failed to get widget data: {e}"
|
|
)
|
|
|
|
|
|
@router.post("/{pipeline_id}/enable")
|
|
async def enable_pipeline(pipeline_id: str) -> dict[str, str]:
|
|
"""Enable a disabled pipeline."""
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
async with _pool.acquire() as conn:
|
|
result = await conn.execute(
|
|
"""
|
|
UPDATE pipeline.registry
|
|
SET is_enabled = TRUE, updated_at = NOW()
|
|
WHERE pipeline_id = $1
|
|
""",
|
|
pipeline_id,
|
|
)
|
|
|
|
if result.split()[-1] == "0":
|
|
raise HTTPException(status_code=404, detail=f"Pipeline not found: {pipeline_id}")
|
|
|
|
# Clear cached instance
|
|
_pipeline_instances.pop(pipeline_id, None)
|
|
|
|
return {"status": "enabled", "pipeline_id": pipeline_id}
|
|
|
|
|
|
@router.post("/{pipeline_id}/disable")
|
|
async def disable_pipeline(pipeline_id: str) -> dict[str, str]:
|
|
"""Disable a pipeline."""
|
|
if not _pool:
|
|
raise HTTPException(status_code=503, detail="Database not initialized")
|
|
|
|
async with _pool.acquire() as conn:
|
|
result = await conn.execute(
|
|
"""
|
|
UPDATE pipeline.registry
|
|
SET is_enabled = FALSE, updated_at = NOW()
|
|
WHERE pipeline_id = $1
|
|
""",
|
|
pipeline_id,
|
|
)
|
|
|
|
if result.split()[-1] == "0":
|
|
raise HTTPException(status_code=404, detail=f"Pipeline not found: {pipeline_id}")
|
|
|
|
# Clear cached instance
|
|
_pipeline_instances.pop(pipeline_id, None)
|
|
|
|
return {"status": "disabled", "pipeline_id": pipeline_id}
|
|
|
|
|
|
@router.get("/{pipeline_id}/health")
|
|
async def pipeline_health(pipeline_id: str) -> dict[str, Any]:
|
|
"""
|
|
Check pipeline health.
|
|
|
|
Returns health status and any issues detected.
|
|
"""
|
|
pipeline = await _get_pipeline_instance(pipeline_id)
|
|
|
|
try:
|
|
health = await pipeline.health_check()
|
|
return {
|
|
"pipeline_id": pipeline_id,
|
|
**health,
|
|
}
|
|
except Exception as e:
|
|
return {
|
|
"pipeline_id": pipeline_id,
|
|
"healthy": False,
|
|
"error": str(e),
|
|
}
|