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.main:app", "--host", "0.0.0.0", "--port", "8000"]
21 changes: 21 additions & 0 deletions QA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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 that all endpoint tests in tests/test_endpoints.py pass and endpoints
return the expected JSON responses.
stack: Python/FastAPI
test_files:
- tests/__init__.py
- tests/conftest.py
- tests/test_endpoints.py
- tests/test_main.py
- tests/test_models.py
- tests/test_storage.py
- tests/test_todos.py
test_runner: pytest tests/
workspace: /tmp/forge-repos/hello-world-fastapi-v4-c3a1e3ed
51 changes: 33 additions & 18 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
# Running the Todo API
# Running the FastAPI 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+ **or** Docker / Docker Compose

## Install dependencies
## Local Setup (without Docker)

```bash
pip install fastapi uvicorn pydantic
```
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # Linux / macOS
# .venv\Scripts\activate # Windows

For running the test suite you will also need:
# Install dependencies
pip install -r requirements.txt

```bash
pip install httpx pytest
# Run the application
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

# Run the test suite
pytest tests/
```

## Start the server
## Docker Setup

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
```
# Build and start
docker compose up --build

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

Interactive docs are served at <http://localhost:8000/docs>.
# Run tests inside the container
docker compose run --rm app pytest tests/
```

## Run the tests
## Endpoints

```bash
pytest tests/
```
| Method | Path | Response |
|--------|-----------|-----------------------------------|
| GET | `/health` | `{"status": "ok"}` |
| GET | `/hello` | `{"message": "Hello, world!"}` |
Empty file added app/__init__.py
Empty file.
Empty file added app/api/__init__.py
Empty file.
24 changes: 24 additions & 0 deletions app/api/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""API routes for the FastAPI application.

Defines an APIRouter with the following endpoints:
- GET /health — returns service health status
- GET /hello — returns a greeting message
"""

from __future__ import annotations

from fastapi import APIRouter

router = APIRouter()


@router.get("/health", tags=["health"])
async def health() -> dict:
"""Return the current health status of the service."""
return {"status": "ok"}


@router.get("/hello", tags=["hello"])
async def hello() -> dict:
"""Return a greeting message."""
return {"message": "Hello, world!"}
19 changes: 19 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""FastAPI application entry point.

Creates the FastAPI app instance and includes the API router
from app.api.routes, which defines the /health and /hello endpoints.
"""

from __future__ import annotations

from fastapi import FastAPI

from app.api.routes import router

app = FastAPI(
title="FastAPI App",
description="A simple FastAPI application with health and hello endpoints.",
version="1.0.0",
)

app.include_router(router)
3 changes: 0 additions & 3 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
'unrecognized arguments' when pytest-timeout is not installed.
"""

import sys
from pathlib import Path


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

services:
app:
build: .
ports:
- "8000:8000"
volumes:
- .:/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
2 changes: 0 additions & 2 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
83 changes: 83 additions & 0 deletions tests/test_endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Test suite for the /health and /hello API endpoints.

Uses FastAPI's TestClient to verify that each endpoint returns the
expected HTTP status code and JSON payload.
"""

from __future__ import annotations

import pytest
from fastapi.testclient import TestClient

from app.main import app


@pytest.fixture()
def client() -> TestClient:
"""Return a TestClient instance wired to the FastAPI application."""
return TestClient(app)


# --------------------------------------------------------------------------- #
# /health endpoint tests
# --------------------------------------------------------------------------- #


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


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


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


# --------------------------------------------------------------------------- #
# /hello endpoint tests
# --------------------------------------------------------------------------- #


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


def test_hello_endpoint_json_payload(client: TestClient) -> None:
"""GET /hello must return {'message': 'Hello, world!'}."""
response = client.get("/hello")
assert response.json() == {"message": "Hello, world!"}


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


# --------------------------------------------------------------------------- #
# Method-not-allowed tests
# --------------------------------------------------------------------------- #


@pytest.mark.parametrize("method", ["post", "put", "patch", "delete"])
def test_health_rejects_non_get_methods(client: TestClient, method: str) -> None:
"""Non-GET requests to /health must return 405 Method Not Allowed."""
response = getattr(client, method)("/health")
assert response.status_code == 405


@pytest.mark.parametrize("method", ["post", "put", "patch", "delete"])
def test_hello_rejects_non_get_methods(client: TestClient, method: str) -> None:
"""Non-GET requests to /hello must return 405 Method Not Allowed."""
response = getattr(client, method)("/hello")
assert response.status_code == 405