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/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 77896cf..d3cb4bc 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -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 . - -Interactive docs are served at . +# 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!"}` | 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/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/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bd018db --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/requirements.txt b/requirements.txt index 5a6ebf7..cabc828 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 diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py new file mode 100644 index 0000000..0a95f96 --- /dev/null +++ b/tests/test_endpoints.py @@ -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