Initial commit - production deployment

This commit is contained in:
2026-01-21 17:17:16 +01:00
commit c23d00dd92
2289 changed files with 638440 additions and 0 deletions

View File

@@ -0,0 +1,193 @@
# ================================================================
# services/orchestrator/app/repositories/orchestration_run_repository.py
# ================================================================
"""
Orchestration Run Repository - Database operations for orchestration audit trail
"""
import uuid
from datetime import datetime, date, timezone
from typing import List, Optional, Dict, Any
from sqlalchemy import select, and_, desc, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.orchestration_run import OrchestrationRun, OrchestrationStatus
class OrchestrationRunRepository:
"""Repository for orchestration run operations"""
def __init__(self, db: AsyncSession):
self.db = db
async def create_run(self, run_data: Dict[str, Any]) -> OrchestrationRun:
"""Create a new orchestration run"""
run = OrchestrationRun(**run_data)
self.db.add(run)
await self.db.flush()
return run
async def get_run_by_id(self, run_id: uuid.UUID) -> Optional[OrchestrationRun]:
"""Get orchestration run by ID"""
stmt = select(OrchestrationRun).where(OrchestrationRun.id == run_id)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def update_run(self, run_id: uuid.UUID, updates: Dict[str, Any]) -> Optional[OrchestrationRun]:
"""Update orchestration run"""
run = await self.get_run_by_id(run_id)
if not run:
return None
for key, value in updates.items():
if hasattr(run, key):
setattr(run, key, value)
run.updated_at = datetime.now(timezone.utc)
await self.db.flush()
return run
async def list_runs(
self,
tenant_id: Optional[uuid.UUID] = None,
status: Optional[OrchestrationStatus] = None,
start_date: Optional[date] = None,
end_date: Optional[date] = None,
limit: int = 50,
offset: int = 0
) -> List[OrchestrationRun]:
"""List orchestration runs with filters"""
conditions = []
if tenant_id:
conditions.append(OrchestrationRun.tenant_id == tenant_id)
if status:
conditions.append(OrchestrationRun.status == status)
if start_date:
conditions.append(func.date(OrchestrationRun.started_at) >= start_date)
if end_date:
conditions.append(func.date(OrchestrationRun.started_at) <= end_date)
stmt = (
select(OrchestrationRun)
.where(and_(*conditions) if conditions else True)
.order_by(desc(OrchestrationRun.started_at))
.limit(limit)
.offset(offset)
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_latest_run_for_tenant(self, tenant_id: uuid.UUID) -> Optional[OrchestrationRun]:
"""Get the most recent orchestration run for a tenant"""
stmt = (
select(OrchestrationRun)
.where(OrchestrationRun.tenant_id == tenant_id)
.order_by(desc(OrchestrationRun.started_at))
.limit(1)
)
result = await self.db.execute(stmt)
return result.scalar_one_or_none()
async def generate_run_number(self) -> str:
"""
Generate unique run number atomically using database-level counting.
Uses MAX(run_number) + 1 approach to avoid race conditions
between reading count and inserting new record.
"""
today = date.today()
date_str = today.strftime("%Y%m%d")
# Get the highest run number for today atomically
# Using MAX on run_number suffix to avoid counting which has race conditions
stmt = select(func.max(OrchestrationRun.run_number)).where(
OrchestrationRun.run_number.like(f"ORCH-{date_str}-%")
)
result = await self.db.execute(stmt)
max_run_number = result.scalar()
if max_run_number:
# Extract the numeric suffix and increment it
try:
suffix = int(max_run_number.split('-')[-1])
next_number = suffix + 1
except (ValueError, IndexError):
# Fallback to 1 if parsing fails
next_number = 1
else:
# No runs for today yet
next_number = 1
return f"ORCH-{date_str}-{next_number:04d}"
async def get_failed_runs(self, limit: int = 10) -> List[OrchestrationRun]:
"""Get recent failed orchestration runs"""
stmt = (
select(OrchestrationRun)
.where(OrchestrationRun.status == OrchestrationStatus.failed)
.order_by(desc(OrchestrationRun.started_at))
.limit(limit)
)
result = await self.db.execute(stmt)
return result.scalars().all()
async def get_run_statistics(
self,
start_date: Optional[date] = None,
end_date: Optional[date] = None
) -> Dict[str, Any]:
"""Get orchestration run statistics"""
conditions = []
if start_date:
conditions.append(func.date(OrchestrationRun.started_at) >= start_date)
if end_date:
conditions.append(func.date(OrchestrationRun.started_at) <= end_date)
where_clause = and_(*conditions) if conditions else True
# Total runs
total_stmt = select(func.count(OrchestrationRun.id)).where(where_clause)
total_result = await self.db.execute(total_stmt)
total_runs = total_result.scalar() or 0
# Successful runs
success_stmt = select(func.count(OrchestrationRun.id)).where(
and_(
where_clause,
OrchestrationRun.status == OrchestrationStatus.completed
)
)
success_result = await self.db.execute(success_stmt)
successful_runs = success_result.scalar() or 0
# Failed runs
failed_stmt = select(func.count(OrchestrationRun.id)).where(
and_(
where_clause,
OrchestrationRun.status == OrchestrationStatus.failed
)
)
failed_result = await self.db.execute(failed_stmt)
failed_runs = failed_result.scalar() or 0
# Average duration
avg_duration_stmt = select(func.avg(OrchestrationRun.duration_seconds)).where(
and_(
where_clause,
OrchestrationRun.status == OrchestrationStatus.completed
)
)
avg_duration_result = await self.db.execute(avg_duration_stmt)
avg_duration = avg_duration_result.scalar() or 0
return {
'total_runs': total_runs,
'successful_runs': successful_runs,
'failed_runs': failed_runs,
'success_rate': (successful_runs / total_runs * 100) if total_runs > 0 else 0,
'average_duration_seconds': float(avg_duration) if avg_duration else 0
}