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

EXPOSE 8000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
20 changes: 20 additions & 0 deletions QA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
app_type: web-api
coverage_applies: true
coverage_source: app
coverage_threshold: 70
coverage_tool: pytest-cov
install_steps:
- pip install -r /tmp/forge-repos/hello-world-micro-api-02986f67/requirements.txt
- pip install ruff
lint_tool: ruff check .
notes: Verify that GET /hello returns 200 with 'message' equal to 'hello world' and
a valid ISO 8601 'timestamp', and that a non-existent route returns 404.
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 --tb=short
workspace: /tmp/forge-repos/hello-world-micro-api-02986f67
48 changes: 31 additions & 17 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
# Running the Todo API
# Hello World API

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

- Python 3.10 or later

## Install dependencies
## Quick Start

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

For running the test suite you will also need:
Then open http://localhost:8000/hello

```bash
pip install httpx pytest
```
## API Reference

## Start the server
### GET /hello

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
Returns a JSON object:

```json
{
"message": "hello world",
"timestamp": "2024-01-01T00:00:00+00:00"
}
```

The API will be available at <http://localhost:8000>.
| Field | Type | Description |
|-------------|--------|--------------------------------------|
| message | string | Always `"hello world"` |
| timestamp | string | Current UTC time in ISO 8601 format |

### GET /

Interactive docs are served at <http://localhost:8000/docs>.
Health-check endpoint returning `{"status": "ok"}`.

## Run the tests
## Running Tests

```bash
pytest tests/
pip install -r requirements.txt
pytest tests/ -v --tb=short --cov=app --cov-report=term-missing
```
37 changes: 37 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""FastAPI application with a GET /hello endpoint.

Returns a JSON payload 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", version="1.0.0")


@app.get("/", tags=["root"])
async def root() -> dict:
"""Health-check endpoint returning a simple status object."""
return {"status": "ok"}


@app.get("/hello", response_model=HelloResponse, tags=["hello"])
async def hello() -> HelloResponse:
"""Return a greeting with the current UTC timestamp."""
return HelloResponse(
message="hello world",
timestamp=datetime.datetime.now(datetime.timezone.utc).isoformat(),
)
5 changes: 5 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
import sys
from pathlib import Path

# Ensure the project root is on sys.path so that 'import app' works.
_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
7 changes: 7 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
services:
api:
build: .
ports:
- "8000:8000"
environment:
PYTHONUNBUFFERED: "1"
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.110.0,<1.0.0
uvicorn[standard]>=0.29.0,<1.0.0
httpx>=0.27.0,<1.0.0
pytest>=8.0.0,<9.0.0
pytest-cov>=5.0.0,<6.0.0
81 changes: 81 additions & 0 deletions tests/test_hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Test suite for the Hello World API.

Covers:
- GET /hello returns 200
- Response JSON contains 'message' key with value 'hello world'
- Response JSON contains 'timestamp' key that is a valid ISO 8601 string
- Timestamp is UTC-aware
- Response schema has exactly the expected keys
- GET / health-check returns 200
- POST /hello returns 405 (Method Not Allowed)
- GET on a non-existent route returns 404
"""

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."""
response = client.get("/hello")
assert response.status_code == 200


def test_hello_response_has_message() -> None:
"""Response body 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_iso_timestamp() -> None:
"""Response body must contain 'timestamp' that is a valid ISO 8601 string."""
response = client.get("/hello")
body = response.json()
assert "timestamp" in body
# datetime.fromisoformat will raise ValueError for non-ISO strings
parsed = datetime.datetime.fromisoformat(body["timestamp"])
assert isinstance(parsed, datetime.datetime)


def test_hello_response_timestamp_is_utc() -> None:
"""Parsed timestamp must be timezone-aware with UTC offset of zero."""
response = client.get("/hello")
body = response.json()
parsed = datetime.datetime.fromisoformat(body["timestamp"])
assert parsed.tzinfo is not None
assert parsed.utcoffset() == datetime.timedelta(0)


def test_hello_response_schema_keys() -> 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_root_returns_200() -> None:
"""GET / health-check must return 200 with {'status': 'ok'}."""
response = client.get("/")
assert response.status_code == 200
assert response.json() == {"status": "ok"}


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


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