From 2b2741ab927b9fb03b2e30ce32c02be41239e079 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 04:52:53 +0000 Subject: [PATCH 1/4] feat: Set Up FastAPI App and Main Entry Point Run: c3a1e3ed-31cd-49c4-86dd-e87ffba1291f Task: ed34a80c-1e43-4adb-a979-abba9a95baaa Agent: builder --- Dockerfile | 12 ++++++++ RUNNING.md | 46 +++++++++++++++++++++++-------- app/__init__.py | 0 app/api/__init__.py | 0 app/api/routes.py | 24 ++++++++++++++++ app/main.py | 19 +++++++++++++ docker-compose.yml | 10 +++++++ requirements.txt | 10 +++---- tests/test_endpoints.py | 61 +++++++++++++++++++++++++++++++++++++++++ 9 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/api/__init__.py create mode 100644 app/api/routes.py create mode 100644 app/main.py create mode 100644 docker-compose.yml create mode 100644 tests/test_endpoints.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..94825db --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/RUNNING.md b/RUNNING.md index 77896cf..0b3266e 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,55 @@ -# 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+ (or Docker) +- pip -## Install dependencies +## Local Setup ```bash -pip install fastapi uvicorn pydantic +pip install -r requirements.txt ``` -For running the test suite you will also need: +## Running the App ```bash -pip install httpx pytest +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload ``` -## Start the server +The API will be available at http://localhost:8000. + +### Endpoints + +| Method | Path | Response | +|--------|-----------|----------------------------------| +| GET | `/health` | `{"status": "ok"}` | +| GET | `/hello` | `{"message": "Hello, world!"}` | + +## Running Tests ```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 +pytest tests/ ``` -The API will be available at . +## Docker -Interactive docs are served at . +### Build and Run -## Run the tests +```bash +docker compose up --build +``` + +### Run Tests in Docker ```bash -pytest tests/ +docker compose run --rm web pytest tests/ ``` diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/api/routes.py b/app/api/routes.py new file mode 100644 index 0000000..d60dd16 --- /dev/null +++ b/app/api/routes.py @@ -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!"} diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..d4cfb33 --- /dev/null +++ b/app/main.py @@ -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) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4ebff7c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,10 @@ +version: "3.9" + +services: + web: + build: . + ports: + - "8000:8000" + volumes: + - .:/app + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload diff --git a/requirements.txt b/requirements.txt index 5a6ebf7..fd9da7a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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.110.0,<1.0.0 +uvicorn>=0.29.0,<1.0.0 +httpx>=0.27.0,<1.0.0 +pytest>=8.0.0,<9.0.0 diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py new file mode 100644 index 0000000..176ac9c --- /dev/null +++ b/tests/test_endpoints.py @@ -0,0 +1,61 @@ +"""Tests for the /health and /hello API endpoints. + +Uses the FastAPI TestClient to verify correct HTTP status codes +and JSON response bodies. +""" + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_health_endpoint_returns_200() -> None: + """GET /health should return HTTP 200.""" + response = client.get("/health") + assert response.status_code == 200 + + +def test_health_endpoint_returns_correct_json() -> None: + """GET /health should return {'status': 'ok'}.""" + response = client.get("/health") + assert response.json() == {"status": "ok"} + + +def test_health_endpoint_content_type() -> None: + """GET /health should return application/json content type.""" + response = client.get("/health") + assert "application/json" in response.headers["content-type"] + + +def test_hello_endpoint_returns_200() -> None: + """GET /hello should return HTTP 200.""" + response = client.get("/hello") + assert response.status_code == 200 + + +def test_hello_endpoint_returns_correct_json() -> None: + """GET /hello should return {'message': 'Hello, world!'}.""" + response = client.get("/hello") + assert response.json() == {"message": "Hello, world!"} + + +def test_hello_endpoint_content_type() -> None: + """GET /hello should return application/json content type.""" + response = client.get("/hello") + assert "application/json" in response.headers["content-type"] + + +def test_health_post_method_not_allowed() -> None: + """POST /health should return 405 Method Not Allowed.""" + response = client.post("/health") + assert response.status_code == 405 + + +def test_hello_post_method_not_allowed() -> None: + """POST /hello should return 405 Method Not Allowed.""" + response = client.post("/hello") + assert response.status_code == 405 From fbd6fc0edb4ee4dd1669bb16a78bd6d7fab57986 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 04:53:21 +0000 Subject: [PATCH 2/4] feat: Implement /health Endpoint Run: c3a1e3ed-31cd-49c4-86dd-e87ffba1291f Task: 6645e8f7-521c-4c17-a735-f9b2bf29daac Agent: builder --- RUNNING.md | 37 +++++++++++++------------------------ conftest.py | 3 --- requirements.txt | 8 ++++---- tests/test_endpoints.py | 34 ++++++++++++---------------------- 4 files changed, 29 insertions(+), 53 deletions(-) diff --git a/RUNNING.md b/RUNNING.md index 0b3266e..079167e 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -16,40 +16,29 @@ coverage_applies: true ## Local Setup ```bash +# Install dependencies pip install -r requirements.txt -``` - -## Running the App -```bash +# Run the application uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -``` - -The API will be available at http://localhost:8000. - -### Endpoints - -| Method | Path | Response | -|--------|-----------|----------------------------------| -| GET | `/health` | `{"status": "ok"}` | -| GET | `/hello` | `{"message": "Hello, world!"}` | - -## Running Tests -```bash +# Run the tests pytest tests/ ``` -## Docker - -### Build and Run +## Docker Setup ```bash +# Build and start docker compose up --build -``` - -### Run Tests in Docker -```bash +# Run tests inside the container docker compose run --rm web pytest tests/ ``` + +## Endpoints + +| Method | Path | Description | Response | +|--------|-----------|-------------------------|---------------------------------| +| GET | `/health` | Service health check | `{"status": "ok"}` | +| GET | `/hello` | Greeting message | `{"message": "Hello, world!"}` | diff --git a/conftest.py b/conftest.py index 2cb6af4..5d91e60 100644 --- a/conftest.py +++ b/conftest.py @@ -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.""" diff --git a/requirements.txt b/requirements.txt index fd9da7a..065bd9f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -fastapi>=0.110.0,<1.0.0 -uvicorn>=0.29.0,<1.0.0 -httpx>=0.27.0,<1.0.0 -pytest>=8.0.0,<9.0.0 +fastapi>=0.100.0 +uvicorn>=0.23.0 +httpx>=0.24.0 +pytest>=7.0.0 diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 176ac9c..79a8cd0 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,6 +1,6 @@ -"""Tests for the /health and /hello API endpoints. +"""Tests for the /health and /hello endpoints. -Uses the FastAPI TestClient to verify correct HTTP status codes +Uses FastAPI's TestClient to verify correct HTTP status codes and JSON response bodies. """ @@ -13,49 +13,39 @@ client = TestClient(app) -def test_health_endpoint_returns_200() -> None: - """GET /health should return HTTP 200.""" +def test_health_endpoint() -> None: + """GET /health returns 200 and {'status': 'ok'}.""" response = client.get("/health") assert response.status_code == 200 - - -def test_health_endpoint_returns_correct_json() -> None: - """GET /health should return {'status': 'ok'}.""" - response = client.get("/health") assert response.json() == {"status": "ok"} def test_health_endpoint_content_type() -> None: - """GET /health should return application/json content type.""" + """GET /health returns application/json content type.""" response = client.get("/health") assert "application/json" in response.headers["content-type"] -def test_hello_endpoint_returns_200() -> None: - """GET /hello should return HTTP 200.""" +def test_hello_endpoint() -> None: + """GET /hello returns 200 and {'message': 'Hello, world!'}.""" response = client.get("/hello") assert response.status_code == 200 - - -def test_hello_endpoint_returns_correct_json() -> None: - """GET /hello should return {'message': 'Hello, world!'}.""" - response = client.get("/hello") assert response.json() == {"message": "Hello, world!"} def test_hello_endpoint_content_type() -> None: - """GET /hello should return application/json content type.""" + """GET /hello returns application/json content type.""" response = client.get("/hello") assert "application/json" in response.headers["content-type"] -def test_health_post_method_not_allowed() -> None: - """POST /health should return 405 Method Not Allowed.""" +def test_health_post_not_allowed() -> None: + """POST /health returns 405 Method Not Allowed.""" response = client.post("/health") assert response.status_code == 405 -def test_hello_post_method_not_allowed() -> None: - """POST /hello should return 405 Method Not Allowed.""" +def test_hello_post_not_allowed() -> None: + """POST /hello returns 405 Method Not Allowed.""" response = client.post("/hello") assert response.status_code == 405 From cee2e870e18745d3a191e62ae6d89f2d0ba8e5d7 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 04:53:52 +0000 Subject: [PATCH 3/4] feat: Implement /hello Endpoint Run: c3a1e3ed-31cd-49c4-86dd-e87ffba1291f Task: e16b74dc-7039-4a39-9104-2f2682344b36 Agent: builder --- RUNNING.md | 39 ++++++++++++++++++++++++++------------- tests/test_endpoints.py | 40 +++++++++++++++++++--------------------- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/RUNNING.md b/RUNNING.md index 079167e..2ead34a 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,4 +1,4 @@ -# Running the Application +# Running the FastAPI Application ## TEAM_BRIEF stack: Python/FastAPI @@ -10,20 +10,21 @@ coverage_applies: true ## Prerequisites -- Python 3.11+ (or Docker) -- pip +- Python 3.11+ **or** Docker / Docker Compose -## Local Setup +## Local Setup (without Docker) ```bash +# Create and activate a virtual environment +python -m venv .venv +source .venv/bin/activate # Linux/macOS +# .venv\Scripts\activate # Windows + # Install dependencies pip install -r requirements.txt # Run the application uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload - -# Run the tests -pytest tests/ ``` ## Docker Setup @@ -32,13 +33,25 @@ pytest tests/ # Build and start docker compose up --build -# Run tests inside the container +# The API will be available at http://localhost:8000 +``` + +## Running Tests + +```bash +# Local +pytest tests/ + +# With coverage +pytest tests/ --cov=app --cov-report=term-missing + +# Inside Docker docker compose run --rm web pytest tests/ ``` -## Endpoints +## Available Endpoints -| Method | Path | Description | Response | -|--------|-----------|-------------------------|---------------------------------| -| GET | `/health` | Service health check | `{"status": "ok"}` | -| GET | `/hello` | Greeting message | `{"message": "Hello, world!"}` | +| Method | Path | Description | +|--------|-----------|--------------------------------------| +| GET | `/health` | Returns `{"status": "ok"}` | +| GET | `/hello` | Returns `{"message": "Hello, world!"}` | diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 79a8cd0..88381e8 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,6 +1,6 @@ -"""Tests for the /health and /hello endpoints. +"""Tests for the /health and /hello API endpoints. -Uses FastAPI's TestClient to verify correct HTTP status codes +Uses the FastAPI TestClient to verify correct HTTP status codes and JSON response bodies. """ @@ -13,39 +13,37 @@ client = TestClient(app) -def test_health_endpoint() -> None: - """GET /health returns 200 and {'status': 'ok'}.""" +def test_health_endpoint_status_code() -> None: + """GET /health should return HTTP 200.""" response = client.get("/health") assert response.status_code == 200 - assert response.json() == {"status": "ok"} -def test_health_endpoint_content_type() -> None: - """GET /health returns application/json content type.""" +def test_health_endpoint_json() -> None: + """GET /health should return {'status': 'ok'}.""" response = client.get("/health") - assert "application/json" in response.headers["content-type"] + assert response.json() == {"status": "ok"} -def test_hello_endpoint() -> None: - """GET /hello returns 200 and {'message': 'Hello, world!'}.""" +def test_hello_endpoint_status_code() -> None: + """GET /hello should return HTTP 200.""" response = client.get("/hello") assert response.status_code == 200 + + +def test_hello_endpoint_json() -> None: + """GET /hello should return {'message': 'Hello, world!'}.""" + response = client.get("/hello") assert response.json() == {"message": "Hello, world!"} def test_hello_endpoint_content_type() -> None: - """GET /hello returns application/json content type.""" + """GET /hello should return application/json content type.""" response = client.get("/hello") assert "application/json" in response.headers["content-type"] -def test_health_post_not_allowed() -> None: - """POST /health returns 405 Method Not Allowed.""" - response = client.post("/health") - assert response.status_code == 405 - - -def test_hello_post_not_allowed() -> None: - """POST /hello returns 405 Method Not Allowed.""" - response = client.post("/hello") - assert response.status_code == 405 +def test_health_endpoint_content_type() -> None: + """GET /health should return application/json content type.""" + response = client.get("/health") + assert "application/json" in response.headers["content-type"] From 385cd1a4396b2fd24630afaec6db59924f245561 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 04:54:23 +0000 Subject: [PATCH 4/4] feat: Write Complete Test Suite for Endpoints Run: c3a1e3ed-31cd-49c4-86dd-e87ffba1291f Task: d92781d2-04fd-4d09-b2a3-f5e444f29f51 Agent: builder --- QA.md | 21 +++++++++++++ RUNNING.md | 33 +++++++------------ docker-compose.yml | 2 +- requirements.txt | 2 +- tests/test_endpoints.py | 70 ++++++++++++++++++++++++++++++----------- 5 files changed, 87 insertions(+), 41 deletions(-) create mode 100644 QA.md diff --git a/QA.md b/QA.md new file mode 100644 index 0000000..f3134df --- /dev/null +++ b/QA.md @@ -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 diff --git a/RUNNING.md b/RUNNING.md index 2ead34a..d3cb4bc 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -17,14 +17,17 @@ coverage_applies: true ```bash # Create and activate a virtual environment python -m venv .venv -source .venv/bin/activate # Linux/macOS -# .venv\Scripts\activate # Windows +source .venv/bin/activate # Linux / macOS +# .venv\Scripts\activate # Windows # Install dependencies pip install -r requirements.txt # Run the application uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + +# Run the test suite +pytest tests/ ``` ## Docker Setup @@ -33,25 +36,13 @@ uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # Build and start docker compose up --build -# The API will be available at http://localhost:8000 -``` - -## Running Tests - -```bash -# Local -pytest tests/ - -# With coverage -pytest tests/ --cov=app --cov-report=term-missing - -# Inside Docker -docker compose run --rm web pytest tests/ +# Run tests inside the container +docker compose run --rm app pytest tests/ ``` -## Available Endpoints +## Endpoints -| Method | Path | Description | -|--------|-----------|--------------------------------------| -| GET | `/health` | Returns `{"status": "ok"}` | -| GET | `/hello` | Returns `{"message": "Hello, world!"}` | +| Method | Path | Response | +|--------|-----------|-----------------------------------| +| GET | `/health` | `{"status": "ok"}` | +| GET | `/hello` | `{"message": "Hello, world!"}` | diff --git a/docker-compose.yml b/docker-compose.yml index 4ebff7c..bd018db 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: - web: + app: build: . ports: - "8000:8000" diff --git a/requirements.txt b/requirements.txt index 065bd9f..cabc828 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ fastapi>=0.100.0 uvicorn>=0.23.0 -httpx>=0.24.0 pytest>=7.0.0 +httpx>=0.24.0 diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index 88381e8..0a95f96 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -1,49 +1,83 @@ -"""Tests for the /health and /hello API endpoints. +"""Test suite for the /health and /hello API endpoints. -Uses the FastAPI TestClient to verify correct HTTP status codes -and JSON response bodies. +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 -client = TestClient(app) +@pytest.fixture() +def client() -> TestClient: + """Return a TestClient instance wired to the FastAPI application.""" + return TestClient(app) -def test_health_endpoint_status_code() -> None: - """GET /health should return HTTP 200.""" + +# --------------------------------------------------------------------------- # +# /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() -> None: - """GET /health should return {'status': 'ok'}.""" +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_hello_endpoint_status_code() -> None: - """GET /hello should return HTTP 200.""" +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() -> None: - """GET /hello should return {'message': 'Hello, world!'}.""" +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() -> None: - """GET /hello should return application/json content type.""" +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"] -def test_health_endpoint_content_type() -> None: - """GET /health should return application/json content type.""" - response = client.get("/health") - 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