todo-backend / api\serverless_main.py
muhammadshaheryar's picture
Upload api\serverless_main.py with huggingface_hub
60b27b6 verified
"""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()}