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
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/

EXPOSE 8000

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
43 changes: 31 additions & 12 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
# Running the Todo API
# Running the Application

## TEAM_BRIEF
stack: Python/FastAPI
test_runner: pytest tests/
lint_tool: ruff check .
coverage_tool: pytest-cov
coverage_threshold: 70
coverage_applies: true

## Prerequisites

- Python 3.10 or later
- Python 3.11+
- Docker and Docker Compose (optional, for containerised usage)

## Local Development

## Install dependencies
### Install dependencies

```bash
pip install fastapi uvicorn pydantic
pip install -r requirements.txt
```

For running the test suite you will also need:
### Run the application

```bash
pip install httpx pytest
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

## Start the server
Open http://localhost:8000/health to verify the service is running.

### Run the tests

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
pytest tests/
```

The API will be available at <http://localhost:8000>.
## Docker

Interactive docs are served at <http://localhost:8000/docs>.
### Build and run

## Run the tests
```bash
docker compose up --build
```

The API will be available at http://localhost:8000/health.

### Stop

```bash
pytest tests/
docker compose down
```
Empty file added app/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""FastAPI application entry point.

Creates the FastAPI app instance and defines the /health endpoint.
"""

from __future__ import annotations

from fastapi import FastAPI

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


@app.get("/health", tags=["health"])
async def health() -> dict:
"""Return a simple health-check response.

Returns:
A JSON object with a single key ``status`` set to ``"ok"``.
"""
return {"status": "ok"}
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@

Registers the --timeout option so that pytest does not fail with
'unrecognized arguments' when pytest-timeout is not installed.
Also ensures the project root is on sys.path for imports.
"""

import sys
from pathlib import Path

# Ensure the project root is importable
_PROJECT_ROOT = str(Path(__file__).resolve().parent)
if _PROJECT_ROOT not in sys.path:
sys.path.insert(0, _PROJECT_ROOT)


def pytest_addoption(parser):
"""Register --timeout so pytest doesn't choke when the plugin is absent."""
Expand Down
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "3.9"

services:
api:
build: .
ports:
- "8000:8000"
restart: unless-stopped
11 changes: 11 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""FastAPI application entry point.

Creates the FastAPI app and mounts the Todo CRUD router.
Also re-exports the /health endpoint from app.main.
"""

from __future__ import annotations
Expand All @@ -22,3 +23,13 @@
async def root() -> dict:
"""Return a welcome message at the API root."""
return {"message": "Welcome to the Todo API"}


@app.get("/health", tags=["health"])
async def health() -> dict:
"""Return a simple health-check response.

Returns:
A JSON object with a single key ``status`` set to ``"ok"``.
"""
return {"status": "ok"}
10 changes: 4 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
fastapi>=0.100.0
uvicorn>=0.23.0
pydantic>=2.0.0
pytest>=7.0.0
pytest-timeout>=2.1.0
httpx>=0.24.0
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
pytest>=8.0.0
httpx>=0.27.0
55 changes: 55 additions & 0 deletions tests/test_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""Test suite for the /health endpoint.

Verifies that the GET /health endpoint returns HTTP 200 with the
expected JSON body {"status": "ok"}.
"""

from __future__ import annotations

from fastapi.testclient import TestClient

from app.main import app

client: TestClient = TestClient(app)


def test_health_returns_200() -> None:
"""GET /health must return HTTP 200 status code."""
response = client.get("/health")
assert response.status_code == 200


def test_health_returns_correct_json() -> None:
"""GET /health must return JSON body {"status": "ok"}."""
response = client.get("/health")
assert response.json() == {"status": "ok"}


def test_health_content_type_is_json() -> None:
"""GET /health must return application/json content type."""
response = client.get("/health")
assert "application/json" in response.headers["content-type"]


def test_health_post_method_not_allowed() -> None:
"""POST /health must return 405 Method Not Allowed."""
response = client.post("/health")
assert response.status_code == 405


def test_health_put_method_not_allowed() -> None:
"""PUT /health must return 405 Method Not Allowed."""
response = client.put("/health")
assert response.status_code == 405


def test_health_delete_method_not_allowed() -> None:
"""DELETE /health must return 405 Method Not Allowed."""
response = client.delete("/health")
assert response.status_code == 405


def test_health_patch_method_not_allowed() -> None:
"""PATCH /health must return 405 Method Not Allowed."""
response = client.patch("/health")
assert response.status_code == 405