Spaces:
Running
Running
| from __future__ import annotations | |
| from typing import Any | |
| from fastapi import APIRouter, Depends, HTTPException, Query | |
| from app.api.deps import get_scheduler_service | |
| from app.core.logger import get_logger | |
| from app.services.scheduler_service import ( | |
| SchedulerService, | |
| job_to_response, | |
| history_to_response, | |
| validate_cron_expression, | |
| validate_timezone, | |
| validate_url, | |
| ) | |
| router = APIRouter() | |
| logger = get_logger(__name__) | |
| def _paginate(items: list[Any], total: int, page: int, page_size: int) -> dict[str, Any]: | |
| return { | |
| "items": items, | |
| "total": total, | |
| "page": page, | |
| "page_size": page_size, | |
| "has_next": (page * page_size) < total, | |
| "has_prev": page > 1, | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Scheduler Monitoring | |
| # --------------------------------------------------------------------------- | |
| async def get_scheduler_status( | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| status = scheduler_service.get_scheduler_status() | |
| status["running_job_ids"] = scheduler_service.get_running_job_ids() | |
| return {"success": True, "data": status} | |
| async def get_scheduler_metrics( | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| metrics = await scheduler_service.get_metrics() | |
| return {"success": True, "data": metrics} | |
| async def get_scheduler_health( | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| health = await scheduler_service.get_health() | |
| return {"success": True, "data": health} | |
| # --------------------------------------------------------------------------- | |
| # Job CRUD | |
| # --------------------------------------------------------------------------- | |
| async def create_job( | |
| body: dict[str, Any], | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| errors = _validate_job_create(body) | |
| if errors: | |
| raise HTTPException(status_code=422, detail={"success": False, "errors": errors}) | |
| try: | |
| job = await scheduler_service.create_job(body) | |
| return {"success": True, "data": job_to_response(job)} | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"success": False, "error": str(exc)}) from exc | |
| async def list_jobs( | |
| status: str | None = Query(None, description="Filter by status (active, paused, completed, failed, deleted)"), | |
| tags: str | None = Query(None, description="Comma-separated tags to filter by"), | |
| page: int = Query(1, ge=1, description="Page number"), | |
| page_size: int = Query(20, ge=1, le=100, description="Items per page"), | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| tag_list = [t.strip() for t in tags.split(",") if t.strip()] if tags else None | |
| jobs, total = await scheduler_service.list_jobs( | |
| status=status, tags=tag_list, page=page, page_size=page_size, | |
| ) | |
| return { | |
| "success": True, | |
| "data": _paginate( | |
| items=[job_to_response(j) for j in jobs], | |
| total=total, page=page, page_size=page_size, | |
| ), | |
| } | |
| async def get_job( | |
| job_id: str, | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| job = await scheduler_service.get_job(job_id) | |
| if not job or job.get("status") == "deleted": | |
| raise HTTPException(status_code=404, detail={"success": False, "error": "Job not found"}) | |
| return {"success": True, "data": job_to_response(job)} | |
| async def update_job( | |
| job_id: str, | |
| body: dict[str, Any], | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| try: | |
| job = await scheduler_service.update_job(job_id, body) | |
| return {"success": True, "data": job_to_response(job)} | |
| except KeyError as exc: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": str(exc)}) from exc | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"success": False, "error": str(exc)}) from exc | |
| async def delete_job( | |
| job_id: str, | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| try: | |
| await scheduler_service.delete_job(job_id) | |
| return {"success": True, "message": "Job deleted successfully"} | |
| except KeyError as exc: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": str(exc)}) from exc | |
| async def hard_delete_job( | |
| job_id: str, | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| try: | |
| await scheduler_service.hard_delete_job(job_id) | |
| return {"success": True, "message": "Job permanently deleted"} | |
| except KeyError as exc: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": str(exc)}) from exc | |
| # --------------------------------------------------------------------------- | |
| # Job Control | |
| # --------------------------------------------------------------------------- | |
| async def pause_job( | |
| job_id: str, | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| try: | |
| job = await scheduler_service.pause_job(job_id) | |
| return {"success": True, "data": job_to_response(job)} | |
| except KeyError as exc: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": str(exc)}) from exc | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"success": False, "error": str(exc)}) from exc | |
| async def resume_job( | |
| job_id: str, | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| try: | |
| job = await scheduler_service.resume_job(job_id) | |
| return {"success": True, "data": job_to_response(job)} | |
| except KeyError as exc: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": str(exc)}) from exc | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"success": False, "error": str(exc)}) from exc | |
| async def run_job_now( | |
| job_id: str, | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| try: | |
| await scheduler_service.run_job_now(job_id) | |
| return {"success": True, "message": "Job execution triggered"} | |
| except KeyError as exc: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": str(exc)}) from exc | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail={"success": False, "error": str(exc)}) from exc | |
| # --------------------------------------------------------------------------- | |
| # Execution History | |
| # --------------------------------------------------------------------------- | |
| async def get_job_history( | |
| job_id: str, | |
| page: int = Query(1, ge=1, description="Page number"), | |
| page_size: int = Query(20, ge=1, le=100, description="Items per page"), | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| job = await scheduler_service.get_job(job_id) | |
| if not job: | |
| raise HTTPException(status_code=404, detail={"success": False, "error": "Job not found"}) | |
| history, total = await scheduler_service.get_job_history( | |
| job_id=job_id, page=page, page_size=page_size, | |
| ) | |
| return { | |
| "success": True, | |
| "data": _paginate( | |
| items=[history_to_response(h) for h in history], | |
| total=total, page=page, page_size=page_size, | |
| ), | |
| } | |
| async def get_execution_history( | |
| page: int = Query(1, ge=1, description="Page number"), | |
| page_size: int = Query(50, ge=1, le=200, description="Items per page"), | |
| status: str | None = Query(None, description="Filter by execution status"), | |
| scheduler_service: SchedulerService = Depends(get_scheduler_service), | |
| ): | |
| history, total = await scheduler_service.get_execution_history( | |
| page=page, page_size=page_size, status=status, | |
| ) | |
| return { | |
| "success": True, | |
| "data": _paginate( | |
| items=[history_to_response(h) for h in history], | |
| total=total, page=page, page_size=page_size, | |
| ), | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Validation | |
| # --------------------------------------------------------------------------- | |
| def _validate_job_create(body: dict[str, Any]) -> list[str]: | |
| errors: list[str] = [] | |
| if not body.get("name"): | |
| errors.append("name is required") | |
| elif not isinstance(body["name"], str) or len(body["name"]) > 255: | |
| errors.append("name must be a string between 1 and 255 characters") | |
| if not body.get("url"): | |
| errors.append("url is required") | |
| elif not isinstance(body["url"], str): | |
| errors.append("url must be a string") | |
| else: | |
| try: | |
| validate_url(body["url"]) | |
| except ValueError as exc: | |
| errors.append(str(exc)) | |
| trigger = body.get("trigger") | |
| if not trigger: | |
| errors.append("trigger is required") | |
| elif not isinstance(trigger, dict): | |
| errors.append("trigger must be an object") | |
| else: | |
| trigger_type = trigger.get("type") | |
| if not trigger_type: | |
| errors.append("trigger.type is required") | |
| elif trigger_type not in ("cron", "interval", "date"): | |
| errors.append("trigger.type must be one of: cron, interval, date") | |
| elif trigger_type == "cron": | |
| if not trigger.get("cron_expression"): | |
| errors.append("trigger.cron_expression is required for cron trigger") | |
| else: | |
| try: | |
| validate_cron_expression(trigger["cron_expression"]) | |
| except ValueError as exc: | |
| errors.append(str(exc)) | |
| if body.get("timezone"): | |
| try: | |
| validate_timezone(body["timezone"]) | |
| except ValueError as exc: | |
| errors.append(str(exc)) | |
| timeout = body.get("timeout") | |
| if timeout is not None: | |
| if not isinstance(timeout, (int, float)): | |
| errors.append("timeout must be a number") | |
| elif timeout < 1 or timeout > 300: | |
| errors.append("timeout must be between 1 and 300") | |
| method = body.get("method") | |
| if method and method not in ("GET", "POST", "PUT", "PATCH", "DELETE"): | |
| errors.append("method must be one of: GET, POST, PUT, PATCH, DELETE") | |
| retry = body.get("retry") | |
| if retry is not None: | |
| if not isinstance(retry, dict): | |
| errors.append("retry must be an object") | |
| else: | |
| max_retries = retry.get("max_retries") | |
| if max_retries is not None and (not isinstance(max_retries, int) or max_retries < 0 or max_retries > 10): | |
| errors.append("retry.max_retries must be between 0 and 10") | |
| return errors | |