Files
bakery-ia/services/external/app/api/poi_refresh_jobs.py

442 lines
13 KiB
Python
Raw Normal View History

"""
POI Refresh Jobs API Endpoints
REST API for managing POI refresh background jobs.
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, desc
from typing import List, Optional
from datetime import datetime, timezone
from pydantic import BaseModel, Field
import structlog
import uuid
from app.core.database import get_db
from app.services.poi_refresh_service import POIRefreshService
from app.services.poi_scheduler import get_scheduler
from app.models.poi_refresh_job import POIRefreshJob
logger = structlog.get_logger()
router = APIRouter(prefix="/poi-refresh-jobs", tags=["POI Refresh Jobs"])
# Response Models
class POIRefreshJobResponse(BaseModel):
"""POI refresh job response"""
id: str
tenant_id: str
status: str
scheduled_at: datetime
started_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
attempt_count: int
max_attempts: int
pois_detected: Optional[int] = None
changes_detected: bool = False
change_summary: Optional[dict] = None
error_message: Optional[str] = None
next_scheduled_at: Optional[datetime] = None
duration_seconds: Optional[float] = None
is_overdue: bool
can_retry: bool
class Config:
from_attributes = True
class ScheduleJobRequest(BaseModel):
"""Schedule POI refresh job request"""
tenant_id: str = Field(..., description="Tenant UUID")
latitude: float = Field(..., ge=-90, le=90, description="Bakery latitude")
longitude: float = Field(..., ge=-180, le=180, description="Bakery longitude")
scheduled_at: Optional[datetime] = Field(None, description="When to run (default: 180 days from now)")
class JobExecutionResult(BaseModel):
"""Job execution result"""
status: str
job_id: str
message: Optional[str] = None
pois_detected: Optional[int] = None
changes_detected: Optional[bool] = None
change_summary: Optional[dict] = None
duration_seconds: Optional[float] = None
next_scheduled_at: Optional[str] = None
error: Optional[str] = None
attempt: Optional[int] = None
can_retry: Optional[bool] = None
# Endpoints
@router.post(
"/schedule",
response_model=POIRefreshJobResponse,
summary="Schedule POI refresh job",
description="Schedule a background job to refresh POI context for a tenant"
)
async def schedule_refresh_job(
request: ScheduleJobRequest,
db: AsyncSession = Depends(get_db)
):
"""
Schedule a POI refresh job for a tenant.
Creates a background job that will detect POIs for the tenant's location
at the scheduled time. Default schedule is 180 days from now.
"""
try:
tenant_uuid = uuid.UUID(request.tenant_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid tenant_id format")
try:
poi_refresh_service = POIRefreshService()
job = await poi_refresh_service.schedule_refresh_job(
tenant_id=request.tenant_id,
latitude=request.latitude,
longitude=request.longitude,
scheduled_at=request.scheduled_at,
session=db
)
logger.info(
"POI refresh job scheduled via API",
tenant_id=request.tenant_id,
job_id=str(job.id),
scheduled_at=job.scheduled_at
)
return POIRefreshJobResponse(
id=str(job.id),
tenant_id=str(job.tenant_id),
status=job.status,
scheduled_at=job.scheduled_at,
started_at=job.started_at,
completed_at=job.completed_at,
attempt_count=job.attempt_count,
max_attempts=job.max_attempts,
pois_detected=job.pois_detected,
changes_detected=job.changes_detected,
change_summary=job.change_summary,
error_message=job.error_message,
next_scheduled_at=job.next_scheduled_at,
duration_seconds=job.duration_seconds,
is_overdue=job.is_overdue,
can_retry=job.can_retry
)
except Exception as e:
logger.error(
"Failed to schedule POI refresh job",
tenant_id=request.tenant_id,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Failed to schedule refresh job: {str(e)}"
)
@router.get(
"/{job_id}",
response_model=POIRefreshJobResponse,
summary="Get refresh job by ID",
description="Retrieve details of a specific POI refresh job"
)
async def get_refresh_job(
job_id: str,
db: AsyncSession = Depends(get_db)
):
"""Get POI refresh job by ID"""
try:
job_uuid = uuid.UUID(job_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid job_id format")
result = await db.execute(
select(POIRefreshJob).where(POIRefreshJob.id == job_uuid)
)
job = result.scalar_one_or_none()
if not job:
raise HTTPException(status_code=404, detail=f"Job not found: {job_id}")
return POIRefreshJobResponse(
id=str(job.id),
tenant_id=str(job.tenant_id),
status=job.status,
scheduled_at=job.scheduled_at,
started_at=job.started_at,
completed_at=job.completed_at,
attempt_count=job.attempt_count,
max_attempts=job.max_attempts,
pois_detected=job.pois_detected,
changes_detected=job.changes_detected,
change_summary=job.change_summary,
error_message=job.error_message,
next_scheduled_at=job.next_scheduled_at,
duration_seconds=job.duration_seconds,
is_overdue=job.is_overdue,
can_retry=job.can_retry
)
@router.get(
"/tenant/{tenant_id}",
response_model=List[POIRefreshJobResponse],
summary="Get refresh jobs for tenant",
description="Retrieve all POI refresh jobs for a specific tenant"
)
async def get_tenant_refresh_jobs(
tenant_id: str,
status: Optional[str] = Query(None, description="Filter by status"),
limit: int = Query(50, ge=1, le=200, description="Maximum number of results"),
db: AsyncSession = Depends(get_db)
):
"""Get all POI refresh jobs for a tenant"""
try:
tenant_uuid = uuid.UUID(tenant_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid tenant_id format")
query = select(POIRefreshJob).where(POIRefreshJob.tenant_id == tenant_uuid)
if status:
query = query.where(POIRefreshJob.status == status)
query = query.order_by(desc(POIRefreshJob.scheduled_at)).limit(limit)
result = await db.execute(query)
jobs = result.scalars().all()
return [
POIRefreshJobResponse(
id=str(job.id),
tenant_id=str(job.tenant_id),
status=job.status,
scheduled_at=job.scheduled_at,
started_at=job.started_at,
completed_at=job.completed_at,
attempt_count=job.attempt_count,
max_attempts=job.max_attempts,
pois_detected=job.pois_detected,
changes_detected=job.changes_detected,
change_summary=job.change_summary,
error_message=job.error_message,
next_scheduled_at=job.next_scheduled_at,
duration_seconds=job.duration_seconds,
is_overdue=job.is_overdue,
can_retry=job.can_retry
)
for job in jobs
]
@router.post(
"/{job_id}/execute",
response_model=JobExecutionResult,
summary="Execute refresh job",
description="Manually trigger execution of a pending POI refresh job"
)
async def execute_refresh_job(
job_id: str,
db: AsyncSession = Depends(get_db)
):
"""Manually execute a POI refresh job"""
try:
job_uuid = uuid.UUID(job_id)
except ValueError:
raise HTTPException(status_code=400, detail="Invalid job_id format")
try:
poi_refresh_service = POIRefreshService()
result = await poi_refresh_service.execute_refresh_job(
job_id=job_id,
session=db
)
logger.info(
"POI refresh job executed via API",
job_id=job_id,
status=result["status"]
)
return JobExecutionResult(**result)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
except Exception as e:
logger.error(
"Failed to execute POI refresh job",
job_id=job_id,
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Failed to execute refresh job: {str(e)}"
)
@router.post(
"/process-pending",
summary="Process all pending jobs",
description="Manually trigger processing of all pending POI refresh jobs"
)
async def process_pending_jobs(
max_concurrent: int = Query(5, ge=1, le=20, description="Max concurrent executions"),
db: AsyncSession = Depends(get_db)
):
"""Process all pending POI refresh jobs"""
try:
poi_refresh_service = POIRefreshService()
result = await poi_refresh_service.process_pending_jobs(
max_concurrent=max_concurrent,
session=db
)
logger.info(
"Pending POI refresh jobs processed via API",
total_jobs=result["total_jobs"],
successful=result["successful"],
failed=result["failed"]
)
return result
except Exception as e:
logger.error(
"Failed to process pending POI refresh jobs",
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Failed to process pending jobs: {str(e)}"
)
@router.get(
"/pending",
response_model=List[POIRefreshJobResponse],
summary="Get pending jobs",
description="Retrieve all pending POI refresh jobs that are due for execution"
)
async def get_pending_jobs(
limit: int = Query(100, ge=1, le=500, description="Maximum number of results"),
db: AsyncSession = Depends(get_db)
):
"""Get all pending POI refresh jobs"""
try:
poi_refresh_service = POIRefreshService()
jobs = await poi_refresh_service.get_pending_jobs(
limit=limit,
session=db
)
return [
POIRefreshJobResponse(
id=str(job.id),
tenant_id=str(job.tenant_id),
status=job.status,
scheduled_at=job.scheduled_at,
started_at=job.started_at,
completed_at=job.completed_at,
attempt_count=job.attempt_count,
max_attempts=job.max_attempts,
pois_detected=job.pois_detected,
changes_detected=job.changes_detected,
change_summary=job.change_summary,
error_message=job.error_message,
next_scheduled_at=job.next_scheduled_at,
duration_seconds=job.duration_seconds,
is_overdue=job.is_overdue,
can_retry=job.can_retry
)
for job in jobs
]
except Exception as e:
logger.error(
"Failed to get pending POI refresh jobs",
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Failed to get pending jobs: {str(e)}"
)
@router.post(
"/trigger-scheduler",
summary="Trigger scheduler immediately",
description="Trigger an immediate check for pending jobs (bypasses schedule)"
)
async def trigger_scheduler():
"""Trigger POI refresh scheduler immediately"""
try:
scheduler = get_scheduler()
if not scheduler.is_running:
raise HTTPException(
status_code=503,
detail="POI refresh scheduler is not running"
)
result = await scheduler.trigger_immediate_check()
logger.info(
"POI refresh scheduler triggered via API",
total_jobs=result["total_jobs"],
successful=result["successful"],
failed=result["failed"]
)
return result
except HTTPException:
raise
except Exception as e:
logger.error(
"Failed to trigger POI refresh scheduler",
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Failed to trigger scheduler: {str(e)}"
)
@router.get(
"/scheduler/status",
summary="Get scheduler status",
description="Check if POI refresh scheduler is running"
)
async def get_scheduler_status():
"""Get POI refresh scheduler status"""
try:
scheduler = get_scheduler()
return {
"is_running": scheduler.is_running,
"check_interval_seconds": scheduler.check_interval_seconds,
"max_concurrent_jobs": scheduler.max_concurrent_jobs
}
except Exception as e:
logger.error(
"Failed to get scheduler status",
error=str(e),
exc_info=True
)
raise HTTPException(
status_code=500,
detail=f"Failed to get scheduler status: {str(e)}"
)