Spaces:
Runtime error
Runtime error
File size: 10,423 Bytes
60b27b6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | """Serverless-compatible FastAPI application for Vercel deployment."""
import os
import sys
from pathlib import Path
# Add src to path for imports
src_path = Path(__file__).parent.parent / "src"
sys.path.insert(0, str(src_path))
from contextlib import asynccontextmanager
from datetime import datetime
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
# Import components - delay heavy imports until needed to avoid initialization issues
def get_database_components():
from database import get_db, engine, Base
return get_db, engine, Base
def get_models():
from models.todo import Todo
from models.user import User
return Todo, User
def get_schemas():
from schemas.todo import TodoCreate, TodoUpdate, TodoResponse, TodoListResponse
return TodoCreate, TodoUpdate, TodoResponse, TodoListResponse
def get_services():
from services.todo_service import TodoService
return TodoService
def get_routers():
from api.routes.auth import router as auth_router
from api.chat_router import router as chat_router
from api.task_router import router as task_router
return auth_router, chat_router, task_router
def get_middlewares():
from middleware.auth import get_current_user
return get_current_user
app = FastAPI(
title="Todo API",
description="REST API for managing todo items with extended features",
version="2.0.0"
)
# Configure CORS for production - allow your frontend domain
# For production, replace with your actual frontend URL
frontend_url = os.getenv("FRONTEND_URL", "https://localhost:3000")
allow_origins = [frontend_url]
# Add localhost origins for development if in development mode
environment = os.getenv("ENVIRONMENT", "development")
if environment.lower() != "production":
allow_origins.extend([
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:3002",
"http://localhost:3006",
"http://localhost:3007"
])
app.add_middleware(
CORSMiddleware,
allow_origins=allow_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
auth_router, chat_router, task_router = get_routers()
app.include_router(auth_router, prefix="/api/v1")
app.include_router(chat_router)
app.include_router(task_router, prefix="/api/v1")
def _calculate_overdue(todo, datetime_module=datetime):
"""Calculate if a todo is overdue."""
if todo.due_date is None or todo.completed:
return False
return todo.due_date < datetime_module.utcnow()
def _todo_to_response(todo):
"""Convert Todo model to TodoResponse schema."""
TodoResponse = get_schemas()[2] # Get TodoResponse from schemas
response_data = {
"id": todo.id,
"user_id": todo.user_id,
"title": todo.title,
"description": todo.description,
"completed": todo.completed,
"priority": todo.priority,
"tags": todo.tags,
"due_date": todo.due_date,
"recurrence": todo.recurrence,
"created_at": todo.created_at,
"updated_at": todo.updated_at,
"overdue": _calculate_overdue(todo)
}
return TodoResponse(**response_data)
@app.get("/api/v1/todos", response_model=None) # Will set response_model dynamically
def get_todos(
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]()),
search: Optional[str] = Query(None, description="Search in title and description"),
status: Optional[str] = Query(None, pattern="^(completed|pending)$", description="Filter by status"),
priority: Optional[str] = Query(None, pattern="^(low|medium|high)$", description="Filter by priority"),
due_before: Optional[datetime] = Query(None, description="Filter todos due before this date"),
due_after: Optional[datetime] = Query(None, description="Filter todos due after this date"),
tag: Optional[str] = Query(None, description="Filter by specific tag"),
sort_by: Optional[str] = Query("created_at", pattern="^(created_at|due_date|priority|title)$", description="Sort field"),
sort_order: Optional[str] = Query("desc", pattern="^(asc|desc)$", description="Sort order"),
):
"""Get all todos with optional search, filtering, and sorting."""
TodoListResponse = get_schemas()[3] # Get TodoListResponse from schemas
app.router.routes[-1].response_model = TodoListResponse # Set response model dynamically
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
todos = service.get_all(
search=search,
status=status,
priority=priority,
due_before=due_before,
due_after=due_after,
tag=tag,
sort_by=sort_by,
sort_order=sort_order
)
# Convert to response schema with overdue calculation
todos_response = [_todo_to_response(todo) for todo in todos]
return {
"todos": todos_response,
"count": len(todos_response),
"has_more": False # Pagination not implemented yet
}
@app.post("/api/v1/todos", response_model=None, status_code=201)
def create_todo(
todo_data: object, # Will be validated dynamically
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]())
):
"""Create a new todo with optional extended fields."""
TodoResponse = get_schemas()[2] # Get TodoResponse from schemas
app.router.routes[-1].response_model = TodoResponse # Set response model dynamically
try:
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
todo = service.create(todo_data)
return _todo_to_response(todo)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/v1/todos/{todo_id}", response_model=None)
def get_todo(
todo_id: int,
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]())
):
"""Get a single todo by ID."""
TodoResponse = get_schemas()[2] # Get TodoResponse from schemas
app.router.routes[-1].response_model = TodoResponse # Set response model dynamically
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
todo = service.get_by_id(todo_id)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return _todo_to_response(todo)
@app.put("/api/v1/todos/{todo_id}", response_model=None)
def update_todo(
todo_id: int,
todo_data: object, # Will be validated dynamically
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]())
):
"""Update a todo with optional extended fields."""
TodoResponse = get_schemas()[2] # Get TodoResponse from schemas
app.router.routes[-1].response_model = TodoResponse # Set response model dynamically
try:
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
todo = service.update(todo_id, todo_data)
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return _todo_to_response(todo)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.delete("/api/v1/todos/{todo_id}")
def delete_todo(
todo_id: int,
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]())
):
"""Delete a todo."""
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
if not service.delete(todo_id):
raise HTTPException(status_code=404, detail="Todo not found")
return {"message": "Todo deleted successfully"}
@app.patch("/api/v1/todos/{todo_id}/complete")
def mark_todo_complete(
todo_id: int,
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]())
):
"""
Mark a todo as complete.
If the todo has a recurrence pattern, automatically creates the next instance.
Returns both the completed task and the next task (if applicable).
"""
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
result = service.mark_complete(todo_id)
if result is None:
raise HTTPException(status_code=404, detail="Todo not found")
Todo, _ = get_models() # Get Todo model
TodoResponse = get_schemas()[2] # Get TodoResponse from schemas
completed_task, next_task = result
response = {
"completed_task": _todo_to_response(completed_task)
}
if next_task:
response["next_task"] = _todo_to_response(next_task)
return response
@app.get("/api/v1/todos/due-soon")
def get_todos_due_soon(
hours: int = Query(1, ge=1, le=24, description="Number of hours to look ahead"),
db: Session = Depends(lambda: get_database_components()[0]()),
current_user: object = Depends(lambda: get_middlewares()[0]())
):
"""Get todos due within the specified hours for reminder notifications."""
TodoService = get_services()[0] # Get TodoService from services
service = TodoService(db, user_id=current_user.id)
todos = service.get_todos_due_soon(hours=hours)
todos_response = [_todo_to_response(todo) for todo in todos]
return {"todos": todos_response, "count": len(todos_response)}
@app.get("/")
def root():
"""Root endpoint for health check."""
return {"message": "Todo API is running", "status": "healthy"}
@app.get("/health")
def health_check():
"""Health check endpoint."""
return {"status": "healthy", "timestamp": datetime.now()} |