From 664d60fea11ad68e3a7a06bc7d2e6f18f4aafe6a Mon Sep 17 00:00:00 2001 From: FORGE Date: Fri, 10 Apr 2026 16:15:53 +0000 Subject: [PATCH 1/3] feat: Create FastAPI App and Main Entry Point Run: 77d330c7-97e9-4f49-9f4f-1870691f1b8b Task: 7dcfbe7c-f1ed-460c-af5b-1e024a553b80 Agent: builder --- Dockerfile | 12 +++++++++++ RUNNING.md | 47 +++++++++++++++++++++++++++++++++----------- app/__init__.py | 0 app/api/__init__.py | 0 app/api/health.py | 16 +++++++++++++++ app/api/hello.py | 16 +++++++++++++++ app/main.py | 20 +++++++++++++++++++ conftest.py | 6 ++++++ docker-compose.yml | 11 +++++++++++ requirements.txt | 10 ++++------ tests/test_health.py | 22 +++++++++++++++++++++ tests/test_hello.py | 29 +++++++++++++++++++++++++++ 12 files changed, 171 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/health.py create mode 100644 app/api/hello.py create mode 100644 app/main.py create mode 100644 docker-compose.yml create mode 100644 tests/test_health.py create mode 100644 tests/test_hello.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..385ccd3 --- /dev/null +++ b/Dockerfile @@ -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.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/RUNNING.md b/RUNNING.md index 77896cf..836443c 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,56 @@ -# 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.12+ +- Docker and Docker Compose (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 --reload ``` -## Start the server +The API will be available at: +- http://localhost:8000/hello +- http://localhost:8000/health + +### Run 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 with Docker Compose -## Run the tests +```bash +docker compose up --build +``` + +The API will be accessible at: +- http://localhost:8000/hello +- http://localhost:8000/health + +### Stop the service ```bash -pytest tests/ +docker compose down ``` 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/health.py b/app/api/health.py new file mode 100644 index 0000000..7928ce5 --- /dev/null +++ b/app/api/health.py @@ -0,0 +1,16 @@ +"""Health check endpoint router. + +Provides a simple GET /health endpoint that returns the service status. +""" + +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"} diff --git a/app/api/hello.py b/app/api/hello.py new file mode 100644 index 0000000..900abc0 --- /dev/null +++ b/app/api/hello.py @@ -0,0 +1,16 @@ +"""Hello endpoint router. + +Provides a simple GET /hello endpoint that returns a greeting message. +""" + +from __future__ import annotations + +from fastapi import APIRouter + +router = APIRouter() + + +@router.get("/hello", tags=["hello"]) +async def hello() -> dict: + """Return a hello world greeting message.""" + return {"message": "hello world"} diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..6f77a3f --- /dev/null +++ b/app/main.py @@ -0,0 +1,20 @@ +"""FastAPI application entry point. + +Creates the FastAPI app instance and includes the hello and health routers. +""" + +from __future__ import annotations + +from fastapi import FastAPI + +from app.api.hello import router as hello_router +from app.api.health import router as health_router + +app = FastAPI( + title="Todo API", + description="A simple Todo REST API with in-memory storage.", + version="1.0.0", +) + +app.include_router(hello_router) +app.include_router(health_router) diff --git a/conftest.py b/conftest.py index 2cb6af4..ab16b3f 100644 --- a/conftest.py +++ b/conftest.py @@ -2,11 +2,17 @@ Registers the --timeout option so that pytest does not fail with 'unrecognized arguments' when pytest-timeout is not installed. +Adds the project root to sys.path for proper module resolution. """ import sys from pathlib import Path +# Ensure the project root is on sys.path so that 'app' package 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.""" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4ae249c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: "3.9" + +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "8000:8000" + environment: + - PYTHONUNBUFFERED=1 diff --git a/requirements.txt b/requirements.txt index 5a6ebf7..ecbcde8 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 +pytest>=8.0.0,<9.0.0 +httpx>=0.27.0,<1.0.0 diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..cc46a51 --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,22 @@ +"""Tests for the /health endpoint.""" + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_health_returns_ok() -> None: + """GET /health should return 200 with status ok JSON.""" + response = client.get("/health") + assert response.status_code == 200 + assert response.json() == {"status": "ok"} + + +def test_health_post_not_allowed() -> None: + """POST /health should return 405 Method Not Allowed.""" + response = client.post("/health") + assert response.status_code == 405 diff --git a/tests/test_hello.py b/tests/test_hello.py new file mode 100644 index 0000000..923b1aa --- /dev/null +++ b/tests/test_hello.py @@ -0,0 +1,29 @@ +"""Tests for the /hello endpoint.""" + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_hello_returns_hello_world() -> None: + """GET /hello should return 200 with the expected greeting JSON.""" + response = client.get("/hello") + assert response.status_code == 200 + assert response.json() == {"message": "hello world"} + + +def test_hello_with_extra_query_params() -> None: + """GET /hello with extra query parameters should still return 200 and correct JSON.""" + response = client.get("/hello", params={"foo": "bar", "baz": "1"}) + assert response.status_code == 200 + assert response.json() == {"message": "hello world"} + + +def test_hello_post_not_allowed() -> None: + """POST /hello should return 405 Method Not Allowed.""" + response = client.post("/hello") + assert response.status_code == 405 From 155bebedd4a4710782741c2dffee3abaacb3432b Mon Sep 17 00:00:00 2001 From: FORGE Date: Fri, 10 Apr 2026 16:16:32 +0000 Subject: [PATCH 2/3] feat: Implement /hello and /health Endpoints Run: 77d330c7-97e9-4f49-9f4f-1870691f1b8b Task: 6da121f5-45e0-41e8-a60f-dc0a3efbfe70 Agent: builder --- RUNNING.md | 57 ++++++++++++++++++++++++++++---------------- app/main.py | 4 ++-- docker-compose.yml | 4 +--- requirements.txt | 6 ++--- tests/test_health.py | 22 +++++++++++++---- tests/test_hello.py | 28 ++++++++++++++-------- 6 files changed, 78 insertions(+), 43 deletions(-) diff --git a/RUNNING.md b/RUNNING.md index 836443c..853770b 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -10,47 +10,62 @@ coverage_applies: true ## Prerequisites -- Python 3.12+ -- Docker and Docker Compose (for containerised usage) +- Docker and Docker Compose installed on your machine. -## Local Development - -### Install dependencies +## Running with Docker Compose ```bash -pip install -r requirements.txt +docker compose up --build ``` -### Run the application +The application will be available at: + +- **Hello endpoint:** [http://localhost:8000/hello](http://localhost:8000/hello) +- **Health endpoint:** [http://localhost:8000/health](http://localhost:8000/health) + +To stop the application: ```bash -uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +docker compose down ``` -The API will be available at: -- http://localhost:8000/hello -- http://localhost:8000/health +## Running Locally (without Docker) -### Run tests +1. Create and activate a virtual environment: ```bash -pytest tests/ +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +``` + +2. Install dependencies: + +```bash +pip install -r requirements.txt ``` -## Docker +3. Start the server: + +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` -### Build and run with Docker Compose +## Running Tests ```bash -docker compose up --build +pip install -r requirements.txt +pytest tests/ ``` -The API will be accessible at: -- http://localhost:8000/hello -- http://localhost:8000/health +To run tests with coverage: -### Stop the service +```bash +pip install pytest-cov +pytest tests/ --cov=app --cov-report=term-missing +``` + +## Running Tests in Docker ```bash -docker compose down +docker compose run --rm app pytest tests/ ``` diff --git a/app/main.py b/app/main.py index 6f77a3f..66ddd1c 100644 --- a/app/main.py +++ b/app/main.py @@ -11,8 +11,8 @@ from app.api.health import router as health_router app = FastAPI( - title="Todo API", - description="A simple Todo REST API with in-memory storage.", + title="Hello & Health API", + description="A minimal FastAPI app with /hello and /health endpoints.", version="1.0.0", ) diff --git a/docker-compose.yml b/docker-compose.yml index 4ae249c..fa724ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,7 @@ version: "3.9" services: app: - build: - context: . - dockerfile: Dockerfile + build: . ports: - "8000:8000" environment: diff --git a/requirements.txt b/requirements.txt index ecbcde8..e8b120c 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 -pytest>=8.0.0,<9.0.0 +fastapi>=0.115.0,<1.0.0 +uvicorn>=0.30.0,<1.0.0 httpx>=0.27.0,<1.0.0 +pytest>=8.0.0,<9.0.0 diff --git a/tests/test_health.py b/tests/test_health.py index cc46a51..8faf8c0 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,4 +1,7 @@ -"""Tests for the /health endpoint.""" +"""Tests for the /health endpoint. + +Verifies that GET /health returns HTTP 200 and the expected JSON body. +""" from __future__ import annotations @@ -9,14 +12,25 @@ client = TestClient(app) -def test_health_returns_ok() -> None: - """GET /health should return 200 with status ok JSON.""" +def test_health_returns_200() -> None: + """GET /health should return HTTP 200.""" response = client.get("/health") assert response.status_code == 200 + + +def test_health_returns_correct_json() -> None: + """GET /health should return {"status": "ok"}.""" + response = client.get("/health") assert response.json() == {"status": "ok"} +def test_health_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_health_post_not_allowed() -> None: - """POST /health should return 405 Method Not Allowed.""" + """POST /health should return HTTP 405 Method Not Allowed.""" response = client.post("/health") assert response.status_code == 405 diff --git a/tests/test_hello.py b/tests/test_hello.py index 923b1aa..5924104 100644 --- a/tests/test_hello.py +++ b/tests/test_hello.py @@ -1,4 +1,7 @@ -"""Tests for the /hello endpoint.""" +"""Tests for the /hello endpoint. + +Verifies that GET /hello returns HTTP 200 and the expected JSON body. +""" from __future__ import annotations @@ -9,21 +12,26 @@ client = TestClient(app) -def test_hello_returns_hello_world() -> None: - """GET /hello should return 200 with the expected greeting JSON.""" +def test_hello_returns_200() -> None: + """GET /hello should return HTTP 200.""" response = client.get("/hello") assert response.status_code == 200 + + +def test_hello_returns_correct_json() -> None: + """GET /hello should return {"message": "hello world"}.""" + response = client.get("/hello") assert response.json() == {"message": "hello world"} -def test_hello_with_extra_query_params() -> None: - """GET /hello with extra query parameters should still return 200 and correct JSON.""" - response = client.get("/hello", params={"foo": "bar", "baz": "1"}) +def test_hello_with_query_params() -> None: + """GET /hello with extra query parameters should still return correct response.""" + response = client.get("/hello", params={"extra": "param"}) assert response.status_code == 200 assert response.json() == {"message": "hello world"} -def test_hello_post_not_allowed() -> None: - """POST /hello should return 405 Method Not Allowed.""" - response = client.post("/hello") - assert response.status_code == 405 +def test_hello_content_type() -> None: + """GET /hello should return application/json content type.""" + response = client.get("/hello") + assert "application/json" in response.headers["content-type"] From 8589dc2cb21d6a48ab146c6faddd2611d47c05b0 Mon Sep 17 00:00:00 2001 From: FORGE Date: Fri, 10 Apr 2026 16:17:12 +0000 Subject: [PATCH 3/3] feat: Write Complete Test Suite Run: 77d330c7-97e9-4f49-9f4f-1870691f1b8b Task: 7957888e-ea2c-42c6-8d25-f004e2131dd5 Agent: builder --- RUNNING.md | 67 +++++++++++++-------------------------- requirements.txt | 5 +-- tests/test_health.py | 73 ++++++++++++++++++++++++++++++++----------- tests/test_hello.py | 74 ++++++++++++++++++++++++++++++++------------ 4 files changed, 135 insertions(+), 84 deletions(-) diff --git a/RUNNING.md b/RUNNING.md index 853770b..aab59e4 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -10,62 +10,39 @@ coverage_applies: true ## Prerequisites -- Docker and Docker Compose installed on your machine. +- Python 3.12+ (or Docker) +- pip -## Running with Docker Compose - -```bash -docker compose up --build -``` - -The application will be available at: - -- **Hello endpoint:** [http://localhost:8000/hello](http://localhost:8000/hello) -- **Health endpoint:** [http://localhost:8000/health](http://localhost:8000/health) - -To stop the application: - -```bash -docker compose down -``` - -## Running Locally (without Docker) - -1. Create and activate a virtual environment: - -```bash -python -m venv .venv -source .venv/bin/activate # On Windows: .venv\Scripts\activate -``` - -2. Install dependencies: +## Local Development ```bash +# Install dependencies pip install -r requirements.txt -``` -3. Start the server: - -```bash -uvicorn app.main:app --host 0.0.0.0 --port 8000 -``` +# Run the application +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -## Running Tests +# Run the test suite +pytest tests/ -v -```bash -pip install -r requirements.txt -pytest tests/ +# Run tests with coverage +pytest tests/ -v --cov=app --cov-report=term-missing ``` -To run tests with coverage: +## Docker ```bash -pip install pytest-cov -pytest tests/ --cov=app --cov-report=term-missing +# Build and start +docker compose up --build + +# The API is available at: +# http://localhost:8000/hello +# http://localhost:8000/health ``` -## Running Tests in Docker +## Endpoints -```bash -docker compose run --rm app pytest tests/ -``` +| Method | Path | Response | +|--------|-----------|---------------------------------| +| GET | `/hello` | `{"message": "hello world"}` | +| GET | `/health` | `{"status": "ok"}` | diff --git a/requirements.txt b/requirements.txt index e8b120c..95e453d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ -fastapi>=0.115.0,<1.0.0 -uvicorn>=0.30.0,<1.0.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 +pytest-cov>=5.0.0,<6.0.0 diff --git a/tests/test_health.py b/tests/test_health.py index 8faf8c0..abf6611 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,36 +1,73 @@ """Tests for the /health endpoint. -Verifies that GET /health returns HTTP 200 and the expected JSON body. +Covers happy-path responses, correct content type, method restrictions, +and behaviour when extra query parameters are supplied. """ 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 wired to the FastAPI application.""" + return TestClient(app) -def test_health_returns_200() -> None: - """GET /health should return HTTP 200.""" - response = client.get("/health") - assert response.status_code == 200 +class TestHealthEndpoint: + """Test suite for GET /health.""" -def test_health_returns_correct_json() -> None: - """GET /health should return {"status": "ok"}.""" - response = client.get("/health") - assert response.json() == {"status": "ok"} + def test_health_returns_200(self, client: TestClient) -> None: + """GET /health should return HTTP 200.""" + response = client.get("/health") + assert response.status_code == 200 + def test_health_returns_correct_json(self, client: TestClient) -> None: + """GET /health should return {"status": "ok"}.""" + response = client.get("/health") + assert response.json() == {"status": "ok"} -def test_health_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_health_content_type_is_json(self, client: TestClient) -> None: + """Response Content-Type must be application/json.""" + response = client.get("/health") + assert "application/json" in response.headers["content-type"] + def test_health_with_extra_query_params(self, client: TestClient) -> None: + """Extra query parameters should be ignored; response stays the same.""" + response = client.get("/health", params={"debug": "true"}) + assert response.status_code == 200 + assert response.json() == {"status": "ok"} -def test_health_post_not_allowed() -> None: - """POST /health should return HTTP 405 Method Not Allowed.""" - response = client.post("/health") - assert response.status_code == 405 + def test_health_post_not_allowed(self, client: TestClient) -> None: + """POST /health should return 405 Method Not Allowed.""" + response = client.post("/health") + assert response.status_code == 405 + + def test_health_put_not_allowed(self, client: TestClient) -> None: + """PUT /health should return 405 Method Not Allowed.""" + response = client.put("/health") + assert response.status_code == 405 + + def test_health_delete_not_allowed(self, client: TestClient) -> None: + """DELETE /health should return 405 Method Not Allowed.""" + response = client.delete("/health") + assert response.status_code == 405 + + def test_health_patch_not_allowed(self, client: TestClient) -> None: + """PATCH /health should return 405 Method Not Allowed.""" + response = client.patch("/health") + assert response.status_code == 405 + + def test_health_response_has_status_key(self, client: TestClient) -> None: + """The JSON body must contain exactly the 'status' key.""" + data = client.get("/health").json() + assert list(data.keys()) == ["status"] + + def test_health_status_value_is_ok(self, client: TestClient) -> None: + """The 'status' value must be the string 'ok'.""" + data = client.get("/health").json() + assert data["status"] == "ok" diff --git a/tests/test_hello.py b/tests/test_hello.py index 5924104..ff2db83 100644 --- a/tests/test_hello.py +++ b/tests/test_hello.py @@ -1,37 +1,73 @@ """Tests for the /hello endpoint. -Verifies that GET /hello returns HTTP 200 and the expected JSON body. +Covers happy-path responses, correct content type, method restrictions, +and behaviour when extra query parameters are supplied. """ 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 wired to the FastAPI application.""" + return TestClient(app) -def test_hello_returns_200() -> None: - """GET /hello should return HTTP 200.""" - response = client.get("/hello") - assert response.status_code == 200 +class TestHelloEndpoint: + """Test suite for GET /hello.""" -def test_hello_returns_correct_json() -> None: - """GET /hello should return {"message": "hello world"}.""" - response = client.get("/hello") - assert response.json() == {"message": "hello world"} + def test_hello_returns_200(self, client: TestClient) -> None: + """GET /hello should return HTTP 200.""" + response = client.get("/hello") + assert response.status_code == 200 + def test_hello_returns_correct_json(self, client: TestClient) -> None: + """GET /hello should return {"message": "hello world"}.""" + response = client.get("/hello") + assert response.json() == {"message": "hello world"} -def test_hello_with_query_params() -> None: - """GET /hello with extra query parameters should still return correct response.""" - response = client.get("/hello", params={"extra": "param"}) - assert response.status_code == 200 - assert response.json() == {"message": "hello world"} + def test_hello_content_type_is_json(self, client: TestClient) -> None: + """Response Content-Type must be application/json.""" + response = client.get("/hello") + assert "application/json" in response.headers["content-type"] + def test_hello_with_extra_query_params(self, client: TestClient) -> None: + """Extra query parameters should be ignored; response stays the same.""" + response = client.get("/hello", params={"foo": "bar", "baz": "123"}) + assert response.status_code == 200 + assert response.json() == {"message": "hello world"} -def test_hello_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_hello_post_not_allowed(self, client: TestClient) -> None: + """POST /hello should return 405 Method Not Allowed.""" + response = client.post("/hello") + assert response.status_code == 405 + + def test_hello_put_not_allowed(self, client: TestClient) -> None: + """PUT /hello should return 405 Method Not Allowed.""" + response = client.put("/hello") + assert response.status_code == 405 + + def test_hello_delete_not_allowed(self, client: TestClient) -> None: + """DELETE /hello should return 405 Method Not Allowed.""" + response = client.delete("/hello") + assert response.status_code == 405 + + def test_hello_patch_not_allowed(self, client: TestClient) -> None: + """PATCH /hello should return 405 Method Not Allowed.""" + response = client.patch("/hello") + assert response.status_code == 405 + + def test_hello_response_has_message_key(self, client: TestClient) -> None: + """The JSON body must contain exactly the 'message' key.""" + data = client.get("/hello").json() + assert list(data.keys()) == ["message"] + + def test_hello_message_value_type_is_string(self, client: TestClient) -> None: + """The 'message' value must be a string.""" + data = client.get("/hello").json() + assert isinstance(data["message"], str)