Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions PLANNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Todo API — Architecture Plan

## Overview

A FastAPI-based Todo CRUD API with in-memory dict storage. The API
exposes RESTful endpoints for managing todo items. Storage is ephemeral
and resets on server restart.

## Project Structure

```
app/
├── __init__.py # Package marker
├── main.py # FastAPI application entry point, includes router
├── models.py # Pydantic schemas: TodoCreate, TodoUpdate, TodoResponse
├── storage.py # In-memory storage class with dict and auto-incrementing ID
└── routes.py # API route definitions
tests/
├── test_models.py
├── test_storage.py
└── test_planning_doc.py
PLANNING.md # This file
```

## Data Model

Each todo item has the following fields:

| Field | Type | Description |
|---------------|-----------------|--------------------------------------|
| `id` | `int` | Auto-generated unique identifier |
| `title` | `str` | Title of the todo item (required) |
| `description` | `str \| None` | Optional longer description |
| `completed` | `bool` | Completion status (default: `False`) |

## Pydantic Schemas

### TodoCreate

Used in `POST /todos` request bodies.

- `title: str` — required, min length 1
- `description: str | None = None` — optional

### TodoUpdate

Used in `PUT /todos/{id}` request bodies. All fields optional; only
provided fields are updated (partial update semantics).

- `title: str | None = None`
- `description: str | None = None`
- `completed: bool | None = None`

### TodoResponse

Returned from all endpoints that produce todo data.

- `id: int`
- `title: str`
- `description: str | None`
- `completed: bool`

## API Endpoints

| Method | Path | Description | Success | Error |
|----------|-------------------|--------------------------|---------|-------|
| `GET` | `/todos` | List all todo items | 200 | — |
| `GET` | `/todos/{id}` | Get a single todo by ID | 200 | 404 |
| `POST` | `/todos` | Create a new todo item | 201 | 422 |
| `PUT` | `/todos/{id}` | Update an existing todo | 200 | 404 |
| `DELETE` | `/todos/{id}` | Delete a todo by ID | 200 | 404 |

## Storage

The storage layer uses a Python **dict** (`dict[int, dict]`) as the
in-memory data store. An auto-incrementing integer counter (`_next_id`)
assigns unique IDs to new items.

The `TodoStorage` class exposes five methods:

- `get_all() -> list[TodoResponse]`
- `get_by_id(todo_id: int) -> TodoResponse | None`
- `create(todo: TodoCreate) -> TodoResponse`
- `update(todo_id: int, todo: TodoUpdate) -> TodoResponse | None`
- `delete(todo_id: int) -> bool`

**Note:** Storage is ephemeral. All data is lost when the server process
restarts.

## Error Handling

- **404 Not Found** — returned by `GET /todos/{id}`, `PUT /todos/{id}`,
and `DELETE /todos/{id}` when no todo with the given ID exists.
- **422 Unprocessable Entity** — returned by `POST /todos` and
`PUT /todos/{id}` when the request body fails validation.
58 changes: 58 additions & 0 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Running the Todo API

This document explains how to install dependencies and run the
Todo API application locally.

## Prerequisites

- Python 3.10 or later
- `pip` (bundled with Python)

## 1. Install dependencies

```bash
pip install -r requirements.txt
```

## 2. Run the application

You can start the server in either of two ways:

### Option A — Using the convenience entry point

```bash
python main.py
```

This starts Uvicorn on **http://127.0.0.1:8000** with auto-reload
enabled.

### Option B — Using Uvicorn directly

```bash
uvicorn app.main:app --reload
```

## 3. Explore the API

Once the server is running you can:

- Open the interactive docs at **http://127.0.0.1:8000/docs**
- Open the alternative docs at **http://127.0.0.1:8000/redoc**
- List all todos: `GET /todos`
- Get a single todo: `GET /todos/{id}`
- Create a todo: `POST /todos`
- Update a todo: `PUT /todos/{id}`
- Delete a todo: `DELETE /todos/{id}`

## 4. Seed data

The application ships with a few example todos so the demo is not
empty on first launch. These are loaded into in-memory storage when
the module is imported and will reset every time the server restarts.

## 5. Running the tests

```bash
python -m pytest tests/ -v
```
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Todo API application package."""
18 changes: 18 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""FastAPI application entry point.

Creates the FastAPI app instance and registers the todo CRUD router.
"""

from __future__ import annotations

from fastapi import FastAPI

from app.routes import router

app = FastAPI(
title="Todo API",
description="A simple Todo REST API with in-memory storage.",
version="1.0.0",
)

app.include_router(router)
65 changes: 65 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Pydantic models for the Todo API.

Defines request/response schemas used across the application:
- TodoCreate: fields required when creating a new todo
- TodoUpdate: optional fields for partial updates
- TodoResponse: full todo representation returned to clients
"""

from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, Field


class TodoCreate(BaseModel):
"""Schema for creating a new todo item.

Attributes:
title: The title of the todo item. Required.
description: An optional longer description of the todo item.
"""

title: str = Field(..., min_length=1, description="Title of the todo item")
description: Optional[str] = Field(
default=None, description="Optional description of the todo item"
)


class TodoUpdate(BaseModel):
"""Schema for updating an existing todo item.

All fields are optional; only provided fields will be updated.

Attributes:
title: New title for the todo item.
description: New description for the todo item.
completed: New completion status for the todo item.
"""

title: Optional[str] = Field(
default=None, min_length=1, description="New title of the todo item"
)
description: Optional[str] = Field(
default=None, description="New description of the todo item"
)
completed: Optional[bool] = Field(
default=None, description="New completion status"
)


class TodoResponse(BaseModel):
"""Schema for a todo item returned in API responses.

Attributes:
id: The unique identifier of the todo item.
title: The title of the todo item.
description: The optional description of the todo item.
completed: Whether the todo item has been completed.
"""

id: int
title: str
description: Optional[str] = None
completed: bool
120 changes: 120 additions & 0 deletions app/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""API router for Todo CRUD endpoints.

Defines all REST endpoints for managing todo items:
- GET /todos – list all todos
- GET /todos/{todo_id} – retrieve a single todo
- POST /todos – create a new todo
- PUT /todos/{todo_id} – update an existing todo
- DELETE /todos/{todo_id} – delete a todo
"""

from __future__ import annotations

from typing import List

from fastapi import APIRouter, HTTPException, status

from app.models import TodoCreate, TodoResponse, TodoUpdate
from app.storage import storage

router = APIRouter()


@router.get("/todos", response_model=List[TodoResponse], tags=["todos"])
async def list_todos() -> List[TodoResponse]:
"""Return all todo items.

Returns:
A list of all todos currently in storage. Returns an empty
list when no todos exist.
"""
return storage.get_all()


@router.get("/todos/{todo_id}", response_model=TodoResponse, tags=["todos"])
async def get_todo(todo_id: int) -> TodoResponse:
"""Return a single todo item by its ID.

Args:
todo_id: The unique identifier of the requested todo.

Returns:
The matching TodoResponse.

Raises:
HTTPException: 404 if no todo with the given ID exists.
"""
todo = storage.get_by_id(todo_id)
if todo is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo not found",
)
return todo


@router.post(
"/todos",
response_model=TodoResponse,
status_code=status.HTTP_201_CREATED,
tags=["todos"],
)
async def create_todo(payload: TodoCreate) -> TodoResponse:
"""Create a new todo item.

Args:
payload: The creation payload containing title and optional
description.

Returns:
The newly created TodoResponse with a 201 status code.
"""
return storage.create(payload)


@router.put("/todos/{todo_id}", response_model=TodoResponse, tags=["todos"])
async def update_todo(todo_id: int, payload: TodoUpdate) -> TodoResponse:
"""Update an existing todo item.

Only fields provided in the request body are updated; omitted
fields remain unchanged.

Args:
todo_id: The unique identifier of the todo to update.
payload: The update payload with optional fields.

Returns:
The updated TodoResponse.

Raises:
HTTPException: 404 if no todo with the given ID exists.
"""
todo = storage.update(todo_id, payload)
if todo is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo not found",
)
return todo


@router.delete(
"/todos/{todo_id}",
status_code=status.HTTP_204_NO_CONTENT,
tags=["todos"],
)
async def delete_todo(todo_id: int) -> None:
"""Delete a todo item by its ID.

Args:
todo_id: The unique identifier of the todo to delete.

Raises:
HTTPException: 404 if no todo with the given ID exists.
"""
deleted = storage.delete(todo_id)
if not deleted:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Todo not found",
)
Loading