Module 6: Project — Advanced Task Manager API

Architecture: Models, Data, and Dependencies

Overview

This capsule lays the foundations of the Task Manager API: the Pydantic models that define the shape of your data, the in-memory storage with its helpers, and the dependencies that get injected into every endpoint. Without this base, the code in the capsules that follow has nothing to stand on.


Models: the data contracts

Create app/models.py:

from enum import Enum
from datetime import datetime
from pydantic import BaseModel, Field


class Status(str, Enum):
    pending = "pending"
    in_progress = "in_progress"
    completed = "completed"
    cancelled = "cancelled"


class Priority(str, Enum):
    low = "low"
    medium = "medium"
    high = "high"
    critical = "critical"


class AttachmentInfo(BaseModel):
    filename: str
    original_name: str
    description: str = ""
    content_type: str
    size_bytes: int


class TaskCreate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    description: str = Field(default="", max_length=1000)
    status: Status = Status.pending
    priority: Priority = Priority.medium
    assignee: str | None = None
    tags: list[str] = Field(default_factory=list)

    model_config = {
        "json_schema_extra": {
            "examples": [{
                "title": "Implement authentication",
                "description": "Add JWT to every protected endpoint",
                "priority": "high",
                "assignee": "mike",
                "tags": ["backend", "security"]
            }]
        }
    }


class TaskUpdate(BaseModel):
    title: str = Field(min_length=1, max_length=200)
    description: str = Field(default="", max_length=1000)
    status: Status
    priority: Priority
    assignee: str | None = None
    tags: list[str] = Field(default_factory=list)


class TaskPatch(BaseModel):
    title: str | None = Field(default=None, min_length=1, max_length=200)
    description: str | None = Field(default=None, max_length=1000)
    status: Status | None = None
    priority: Priority | None = None
    assignee: str | None = None
    tags: list[str] | None = None


class TaskResponse(BaseModel):
    id: int
    title: str
    description: str
    status: Status
    priority: Priority
    assignee: str | None
    tags: list[str]
    attachments: list[AttachmentInfo]
    created_at: str
    updated_at: str


class TaskSummary(BaseModel):
    id: int
    title: str
    status: Status
    priority: Priority
    attachments_count: int


class TaskStats(BaseModel):
    total: int
    by_status: dict[str, int]
    by_priority: dict[str, int]
    completion_rate: float
    with_attachments: int

Design decisions

Status and Priority as (str, Enum) — Lets them serialize as strings in JSON and get validated automatically.

Separate modelsTaskCreate (input without id/timestamps), TaskUpdate (full input), TaskPatch (everything optional), TaskResponse (output with everything), TaskSummary (condensed output for lists).

AttachmentInfo — A nested model for attached-file metadata.

TaskStats — The model for the statistics endpoint.


Data: in-memory storage

Create app/data.py:

from datetime import datetime


tasks_db: list[dict] = []

audit_log: list[dict] = []


def get_current_timestamp() -> str:
    return datetime.now().isoformat()


def generate_id() -> int:
    if not tasks_db:
        return 1
    return max(t["id"] for t in tasks_db) + 1


def find_task_by_id(task_id: int) -> dict | None:
    return next((t for t in tasks_db if t["id"] == task_id), None)


def seed_data():
    """Loads the initial data for testing."""
    initial_tasks = [
        {
            "id": 1,
            "title": "Set up the FastAPI project",
            "description": "Initial setup with a professional folder structure",
            "status": "completed",
            "priority": "high",
            "assignee": "mike",
            "tags": ["setup", "backend"],
            "attachments": [],
            "created_at": "2026-03-01T10:00:00",
            "updated_at": "2026-03-01T14:30:00"
        },
        {
            "id": 2,
            "title": "Implement the task CRUD",
            "description": "CREATE, READ, UPDATE, DELETE endpoints with validation",
            "status": "in_progress",
            "priority": "high",
            "assignee": "mike",
            "tags": ["backend", "api"],
            "attachments": [],
            "created_at": "2026-03-02T09:00:00",
            "updated_at": "2026-03-02T09:00:00"
        },
        {
            "id": 3,
            "title": "Add WebSocket notifications",
            "description": "Real-time updates when tasks are created or completed",
            "status": "pending",
            "priority": "medium",
            "assignee": None,
            "tags": ["backend", "realtime"],
            "attachments": [],
            "created_at": "2026-03-03T08:00:00",
            "updated_at": "2026-03-03T08:00:00"
        },
        {
            "id": 4,
            "title": "Write the API documentation",
            "description": "A complete README with usage examples and setup",
            "status": "pending",
            "priority": "low",
            "assignee": "alex",
            "tags": ["docs"],
            "attachments": [],
            "created_at": "2026-03-04T11:00:00",
            "updated_at": "2026-03-04T11:00:00"
        },
        {
            "id": 5,
            "title": "Bug: the status filter doesn't work",
            "description": "GET /tasks?status=completed returns every task",
            "status": "in_progress",
            "priority": "critical",
            "assignee": "mike",
            "tags": ["bug", "urgent"],
            "attachments": [],
            "created_at": "2026-03-05T16:00:00",
            "updated_at": "2026-03-05T16:45:00"
        }
    ]

    tasks_db.clear()
    tasks_db.extend(initial_tasks)


def log_audit(action: str, details: dict):
    audit_log.append({
        "action": action,
        "details": details,
        "timestamp": get_current_timestamp()
    })

Dependencies: reusable logic

Create app/dependencies.py:

from fastapi import HTTPException, Query
from app.data import find_task_by_id


def pagination_params(
    skip: int = Query(default=0, ge=0, description="Records to skip"),
    limit: int = Query(default=20, ge=1, le=100, description="Records per page")
) -> dict:
    return {"skip": skip, "limit": limit}


def get_task_or_404(task_id: int) -> dict:
    task = find_task_by_id(task_id)
    if not task:
        raise HTTPException(
            status_code=404,
            detail={
                "error": "Task not found",
                "task_id": task_id,
                "suggestion": "Check the ID with GET /tasks/"
            }
        )
    return task


def filter_params(
    status: str | None = Query(default=None, description="Filter by status"),
    priority: str | None = Query(default=None, description="Filter by priority"),
    assignee: str | None = Query(default=None, description="Filter by assignee"),
    search: str | None = Query(default=None, description="Search in the title and description"),
    tag: str | None = Query(default=None, description="Filter by tag")
) -> dict:
    return {
        "status": status,
        "priority": priority,
        "assignee": assignee,
        "search": search,
        "tag": tag
    }


def apply_filters(tasks: list[dict], filters: dict) -> list[dict]:
    result = tasks

    if filters["status"]:
        result = [t for t in result if t["status"] == filters["status"]]

    if filters["priority"]:
        result = [t for t in result if t["priority"] == filters["priority"]]

    if filters["assignee"]:
        result = [t for t in result if t.get("assignee") == filters["assignee"]]

    if filters["search"]:
        query = filters["search"].lower()
        result = [
            t for t in result
            if query in t["title"].lower() or query in t.get("description", "").lower()
        ]

    if filters["tag"]:
        result = [t for t in result if filters["tag"] in t.get("tags", [])]

    return result

WebSocket Manager

Create app/websocket_manager.py:

from fastapi import WebSocket
from datetime import datetime


class ConnectionManager:
    def __init__(self):
        self.active_connections: list[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        if websocket in self.active_connections:
            self.active_connections.remove(websocket)

    async def send_personal(self, data: dict, websocket: WebSocket):
        await websocket.send_json(data)

    async def broadcast(self, data: dict):
        disconnected = []
        for connection in self.active_connections:
            try:
                await connection.send_json(data)
            except Exception:
                disconnected.append(connection)
        for conn in disconnected:
            self.disconnect(conn)

    async def broadcast_event(self, event_type: str, payload: dict):
        await self.broadcast({
            "type": event_type,
            "data": payload,
            "timestamp": datetime.now().isoformat(),
            "connections": len(self.active_connections)
        })

    @property
    def connection_count(self) -> int:
        return len(self.active_connections)


ws_manager = ConnectionManager()

Background notifications

Create app/background/__init__.py (empty) and app/background/notifications.py:

import logging
from datetime import datetime
from app.data import audit_log

logger = logging.getLogger("task_manager.background")


def send_email_notification(
    event: str,
    task_title: str,
    task_id: int,
    recipients: list[str] | None = None
):
    if recipients is None:
        recipients = ["admin@taskmanager.dev"]

    logger.info(
        f"[EMAIL] {event} | Task: '{task_title}' (ID: {task_id}) | "
        f"To: {', '.join(recipients)}"
    )

    audit_log.append({
        "action": "email_sent",
        "details": {
            "event": event,
            "task_id": task_id,
            "task_title": task_title,
            "recipients": recipients
        },
        "timestamp": datetime.now().isoformat()
    })


def log_audit_event(action: str, details: dict):
    logger.info(f"[AUDIT] {action} | {details}")

    audit_log.append({
        "action": action,
        "details": details,
        "timestamp": datetime.now().isoformat()
    })

Middleware

Create app/middleware/__init__.py (empty) and app/middleware/logging.py:

import time
import logging
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request

logger = logging.getLogger("task_manager.middleware")


class LoggingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        start_time = time.time()

        response = await call_next(request)

        duration_ms = (time.time() - start_time) * 1000

        logger.info(
            f"{request.method} {request.url.path} "
            f"→ {response.status_code} "
            f"({duration_ms:.1f}ms)"
        )

        response.headers["X-Process-Time-Ms"] = f"{duration_ms:.1f}"

        return response

Routers: init.py

Create app/routers/__init__.py:

from app.routers.tasks import router as tasks_router
from app.routers.uploads import router as uploads_router
from app.routers.websocket import router as ws_router

__all__ = ["tasks_router", "uploads_router", "ws_router"]

Main: assembling everything

Create app/main.py:

import logging
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.routers import tasks_router, uploads_router, ws_router
from app.middleware.logging import LoggingMiddleware
from app.data import seed_data

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s"
)


@asynccontextmanager
async def lifespan(app):
    seed_data()
    logging.getLogger("task_manager").info("Task Manager API started with sample data")
    yield
    logging.getLogger("task_manager").info("Task Manager API stopped")


app = FastAPI(
    title="Task Manager API",
    description="""
## An advanced API for task management

Features:
- **Full CRUD** with filters, pagination, and search
- **WebSocket** for real-time notifications
- **File uploads** to attach documents to tasks
- **Background tasks** for email notifications
- **CSV export** of tasks

### WebSocket
Connect to `ws://host/ws` to receive events in real time.

### Authentication
This version doesn't include auth (see guide #9).
    """,
    version="2.0.0",
    lifespan=lifespan
)

app.add_middleware(LoggingMiddleware)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(tasks_router)
app.include_router(uploads_router)
app.include_router(ws_router)


@app.get("/health", tags=["System"])
async def health_check():
    from app.websocket_manager import ws_manager
    from app.data import tasks_db, audit_log

    return {
        "status": "healthy",
        "version": "2.0.0",
        "tasks_count": len(tasks_db),
        "websocket_connections": ws_manager.connection_count,
        "audit_events": len(audit_log)
    }

Verifying step 1

# Create the structure
mkdir -p app/routers app/middleware app/background app/uploads
touch app/__init__.py app/routers/__init__.py app/middleware/__init__.py app/background/__init__.py

# Check that the imports work
python -c "from app.models import TaskCreate, TaskResponse, Status, Priority; print('Models OK')"
python -c "from app.data import tasks_db, seed_data; seed_data(); print(f'Data OK: {len(tasks_db)} tasks')"
python -c "from app.dependencies import pagination_params, get_task_or_404; print('Dependencies OK')"
python -c "from app.websocket_manager import ws_manager; print('WS Manager OK')"

If everything prints "OK", the base is ready.


What's next?

In Capsule 03 you implement all the CRUD endpoints with advanced response models and background tasks wired in.