| """Progress API endpoints for polling operation status.""" |
|
|
| from datetime import datetime |
|
|
| from fastapi import APIRouter, Header, HTTPException, Response |
| from fastapi import status as http_status |
|
|
| from ..config.logfire_config import get_logger |
| from ..models.progress_models import create_progress_response |
| from ..utils.etag_utils import check_etag, generate_etag |
| from ..utils.progress import ProgressTracker |
|
|
| logger = get_logger(__name__) |
|
|
| router = APIRouter(prefix="/api/progress", tags=["progress"]) |
|
|
|
|
| @router.get("/{operation_id}") |
| async def get_progress(operation_id: str, response: Response, if_none_match: str | None = Header(None)): |
| """ |
| Get progress for an operation with ETag support. |
| |
| Returns progress state with percentage, status, and message. |
| Clients should poll this endpoint to track long-running operations. |
| """ |
| try: |
| logger.info(f"Getting progress for operation | operation_id={operation_id}") |
|
|
| |
| operation = ProgressTracker.get_progress(operation_id) |
|
|
| if not operation: |
| logger.warning(f"Operation not found | operation_id={operation_id}") |
| raise HTTPException(status_code=404, detail={"error": f"Operation {operation_id} not found"}) |
|
|
| |
| operation["progress_id"] = operation_id |
|
|
| |
| operation_type = operation.get("type", "crawl") |
|
|
| |
| progress_response = create_progress_response(operation_type, operation) |
|
|
| |
| response_data = progress_response.model_dump(by_alias=True, exclude_none=True) |
|
|
| |
| if operation_type == "crawl" and operation.get("status") == "code_extraction": |
| logger.info( |
| f"Code extraction response fields: completedSummaries={response_data.get('completedSummaries')}, totalSummaries={response_data.get('totalSummaries')}, codeBlocksFound={response_data.get('codeBlocksFound')}" |
| ) |
|
|
| |
| etag_data = {k: v for k, v in response_data.items() if k != "timestamp"} |
| current_etag = generate_etag(etag_data) |
|
|
| |
| if check_etag(if_none_match, current_etag): |
| response.status_code = http_status.HTTP_304_NOT_MODIFIED |
| response.headers["ETag"] = current_etag |
| response.headers["Cache-Control"] = "no-cache, must-revalidate" |
| return None |
|
|
| |
| response.headers["ETag"] = current_etag |
| response.headers["Last-Modified"] = datetime.utcnow().isoformat() |
| response.headers["Cache-Control"] = "no-cache, must-revalidate" |
|
|
| |
| if operation.get("status") == "running": |
| |
| response.headers["X-Poll-Interval"] = "1000" |
| else: |
| |
| response.headers["X-Poll-Interval"] = "0" |
|
|
| logger.info( |
| f"Progress retrieved | operation_id={operation_id} | status={response_data.get('status')} | progress={response_data.get('progress')}" |
| ) |
|
|
| return response_data |
|
|
| except HTTPException: |
| raise |
| except Exception as e: |
| logger.error(f"Failed to get progress | error={str(e)} | operation_id={operation_id}") |
| raise HTTPException(status_code=500, detail={"error": str(e)}) from e |
|
|
|
|
| @router.get("/") |
| async def list_active_operations(): |
| """ |
| List all active operations. |
| |
| This endpoint is useful for debugging and monitoring active operations. |
| """ |
| try: |
| logger.info("Listing active operations") |
|
|
| |
| active_operations = [] |
|
|
| |
| for op_id, operation in ProgressTracker._progress_states.items(): |
| if operation.get("status") in ["starting", "running", "crawling", "processing"]: |
| active_operations.append( |
| { |
| "operation_id": op_id, |
| "operation_type": operation.get("type", "unknown"), |
| "status": operation.get("status"), |
| "progress": operation.get("progress", 0), |
| "message": operation.get("log", "Processing..."), |
| "started_at": operation.get("start_time"), |
| } |
| ) |
|
|
| logger.info(f"Active operations listed | count={len(active_operations)}") |
|
|
| return { |
| "operations": active_operations, |
| "count": len(active_operations), |
| "timestamp": datetime.utcnow().isoformat(), |
| } |
|
|
| except Exception as e: |
| logger.error(f"Failed to list active operations | error={str(e)}") |
| raise HTTPException(status_code=500, detail={"error": str(e)}) from e |
|
|