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.12-slim

WORKDIR /app

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

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
19 changes: 19 additions & 0 deletions QA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
app_type: web-api
coverage_applies: true
coverage_source: app
coverage_threshold: 70
coverage_tool: pytest-cov
install_steps:
- pip install -r requirements.txt
lint_tool: ruff check .
notes: Verify GET /hello returns 200 with correct JSON containing 'message' and valid
ISO 8601 'timestamp', 404 for non-existent routes, and 405 for wrong HTTP methods.
stack: Python/FastAPI
test_files:
- tests/test_hello.py
- tests/test_main.py
- tests/test_models.py
- tests/test_storage.py
- tests/test_todos.py
test_runner: pytest tests/ -v
workspace: /tmp/forge-repos/hello-world-micro-api-c74aba96
54 changes: 40 additions & 14 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,59 @@
# Running the Todo API
# Hello World API

## Prerequisites
A minimal FastAPI application exposing a single `GET /hello` endpoint.

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

## Install dependencies
## Quick Start

### Local

```bash
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8000
```

Open <http://localhost:8000/hello>

### Docker

```bash
pip install fastapi uvicorn pydantic
docker compose up --build
```

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

```bash
pip install httpx pytest
pip install -r requirements.txt
pytest tests/ -v
```

## Start the server
Or via Docker:

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
docker compose run --rm api pytest tests/ -v
```

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

Interactive docs are served at <http://localhost:8000/docs>.
### GET /hello

## Run the tests
**Response** `200 OK`

```bash
pytest tests/
```json
{
"message": "hello world",
"timestamp": "2025-01-01T00:00:00+00:00"
}
```

| Field | Type | Description |
|-------------|--------|------------------------------------------|
| message | string | Always `"hello world"` |
| timestamp | string | Current UTC time in ISO 8601 format |
38 changes: 38 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""FastAPI application with a single GET /hello endpoint.

Returns a JSON object containing a greeting message and the current
UTC timestamp in ISO-8601 format.
"""

from __future__ import annotations

import datetime

from fastapi import FastAPI
from pydantic import BaseModel


class HelloResponse(BaseModel):
"""Response model for the /hello endpoint."""

message: str
timestamp: str


app = FastAPI(title="Hello World API")


@app.get("/hello", response_model=HelloResponse)
async def hello() -> HelloResponse:
"""Return a hello world message with the current UTC timestamp."""
now = datetime.datetime.now(datetime.timezone.utc)
return HelloResponse(
message="hello world",
timestamp=now.isoformat(),
)


if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=8000)
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
services:
api:
build: .
ports:
- "8000:8000"
11 changes: 5 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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.6
uvicorn[standard]==0.34.0
pydantic==2.10.4
httpx==0.28.1
pytest==8.3.4
109 changes: 109 additions & 0 deletions tests/test_hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Comprehensive tests for the Hello World API /hello endpoint.

Covers:
- GET /hello returns 200
- Response JSON contains 'message' key with value 'hello world'
- Response JSON contains 'timestamp' key with a valid ISO 8601 string (UTC)
- Response body contains exactly the expected keys
- Non-existent routes return 404
- Wrong HTTP method on /hello returns 405
"""

from __future__ import annotations

import datetime

from fastapi.testclient import TestClient

from app import app

client = TestClient(app)


def test_hello_returns_200() -> None:
"""GET /hello must return HTTP 200 OK."""
response = client.get("/hello")
assert response.status_code == 200


def test_hello_response_has_message_field() -> None:
"""Response JSON must contain 'message' equal to 'hello world'."""
response = client.get("/hello")
body = response.json()
assert "message" in body
assert body["message"] == "hello world"


def test_hello_response_has_timestamp_iso8601() -> None:
"""Response JSON must contain 'timestamp' that is a valid ISO 8601 UTC string."""
response = client.get("/hello")
body = response.json()
assert "timestamp" in body

# Must parse without raising an exception
parsed = datetime.datetime.fromisoformat(body["timestamp"])

# Must carry timezone information (UTC)
assert parsed.tzinfo is not None
assert parsed.tzinfo == datetime.timezone.utc


def test_hello_response_keys_exact() -> None:
"""Response JSON must contain exactly 'message' and 'timestamp' keys."""
response = client.get("/hello")
body = response.json()
assert set(body.keys()) == {"message", "timestamp"}


def test_hello_timestamp_is_recent() -> None:
"""The returned timestamp should be within a few seconds of 'now'."""
before = datetime.datetime.now(datetime.timezone.utc)
response = client.get("/hello")
after = datetime.datetime.now(datetime.timezone.utc)

body = response.json()
ts = datetime.datetime.fromisoformat(body["timestamp"])

assert before <= ts <= after


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


def test_nonexistent_route_returns_404() -> None:
"""A request to a route that does not exist must return 404."""
response = client.get("/")
assert response.status_code == 404


def test_nonexistent_route_random_path_returns_404() -> None:
"""A request to an arbitrary undefined path must return 404."""
response = client.get("/nonexistent")
assert response.status_code == 404


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


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


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


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