diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2bf6443 --- /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 app/ ./app/ + +EXPOSE 8000 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/RUNNING.md b/RUNNING.md index 77896cf..7c69da4 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,52 @@ -# 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+ +- Docker and Docker Compose (optional, 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 ``` -## Start the server +Open http://localhost:8000/health to verify the service is running. + +### Run the 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 +``` + +The API will be available at http://localhost:8000/health. + +### Stop ```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/main.py b/app/main.py new file mode 100644 index 0000000..0947ef1 --- /dev/null +++ b/app/main.py @@ -0,0 +1,24 @@ +"""FastAPI application entry point. + +Creates the FastAPI app instance and defines the /health endpoint. +""" + +from __future__ import annotations + +from fastapi import FastAPI + +app = FastAPI( + title="Todo API", + description="A simple Todo REST API with in-memory storage.", + version="1.0.0", +) + + +@app.get("/health", tags=["health"]) +async def health() -> dict: + """Return a simple health-check response. + + Returns: + A JSON object with a single key ``status`` set to ``"ok"``. + """ + return {"status": "ok"} diff --git a/conftest.py b/conftest.py index 2cb6af4..90c35d1 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. +Also ensures the project root is on sys.path for imports. """ import sys from pathlib import Path +# Ensure the project root 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..821d084 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: "3.9" + +services: + api: + build: . + ports: + - "8000:8000" + restart: unless-stopped diff --git a/main.py b/main.py index 2e8fbda..52a885c 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,7 @@ """FastAPI application entry point. Creates the FastAPI app and mounts the Todo CRUD router. +Also re-exports the /health endpoint from app.main. """ from __future__ import annotations @@ -22,3 +23,13 @@ async def root() -> dict: """Return a welcome message at the API root.""" return {"message": "Welcome to the Todo API"} + + +@app.get("/health", tags=["health"]) +async def health() -> dict: + """Return a simple health-check response. + + Returns: + A JSON object with a single key ``status`` set to ``"ok"``. + """ + return {"status": "ok"} diff --git a/requirements.txt b/requirements.txt index 5a6ebf7..3a4da97 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.115.0 +uvicorn[standard]>=0.30.0 +pytest>=8.0.0 +httpx>=0.27.0 diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..ca3e84e --- /dev/null +++ b/tests/test_health.py @@ -0,0 +1,55 @@ +"""Test suite for the /health endpoint. + +Verifies that the GET /health endpoint returns HTTP 200 with the +expected JSON body {"status": "ok"}. +""" + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.main import app + +client: TestClient = TestClient(app) + + +def test_health_returns_200() -> None: + """GET /health must return HTTP 200 status code.""" + response = client.get("/health") + assert response.status_code == 200 + + +def test_health_returns_correct_json() -> None: + """GET /health must return JSON body {"status": "ok"}.""" + response = client.get("/health") + assert response.json() == {"status": "ok"} + + +def test_health_content_type_is_json() -> None: + """GET /health must return application/json content type.""" + response = client.get("/health") + assert "application/json" in response.headers["content-type"] + + +def test_health_post_method_not_allowed() -> None: + """POST /health must return 405 Method Not Allowed.""" + response = client.post("/health") + assert response.status_code == 405 + + +def test_health_put_method_not_allowed() -> None: + """PUT /health must return 405 Method Not Allowed.""" + response = client.put("/health") + assert response.status_code == 405 + + +def test_health_delete_method_not_allowed() -> None: + """DELETE /health must return 405 Method Not Allowed.""" + response = client.delete("/health") + assert response.status_code == 405 + + +def test_health_patch_method_not_allowed() -> None: + """PATCH /health must return 405 Method Not Allowed.""" + response = client.patch("/health") + assert response.status_code == 405