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..69b9fa8 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,97 @@ -# Running the Todo API +# Running the Hello API + +## 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 +- Docker and Docker Compose installed, **or** +- Python 3.11+ installed locally + +--- + +## Docker-based Setup (Recommended) + +### Build and Start the Server + +```bash +docker-compose up --build +``` + +The API will be available at `http://localhost:8000`. + +### Verify the /hello Endpoint + +```bash +curl http://localhost:8000/hello +``` + +Expected response: -## Install dependencies +```json +{"message": "hello"} +``` + +### Run Tests inside Docker ```bash -pip install fastapi uvicorn pydantic +docker-compose run --rm api pytest tests/ -v ``` -For running the test suite you will also need: +### Stop the Server ```bash -pip install httpx pytest +docker-compose down ``` -## Start the server +--- + +## Local Setup (without Docker) + +### Install Dependencies ```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 +pip install -r requirements.txt ``` -The API will be available at . +### Start the Server -Interactive docs are served at . +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` -## Run the tests +### Run Tests ```bash -pytest tests/ +pytest tests/ -v ``` + +--- + +## Project Structure + +``` +. +├── app/ +│ ├── __init__.py +│ └── main.py # FastAPI application with GET /hello +├── tests/ +│ ├── __init__.py +│ └── test_hello.py # Pytest tests for the /hello endpoint +├── conftest.py # Root pytest configuration +├── requirements.txt # Python dependencies +├── Dockerfile # Container image definition +├── docker-compose.yml # Docker Compose orchestration +└── RUNNING.md # This file +``` + +## API Endpoints + +| Method | Path | Description | Response | +|--------|----------|-----------------------------------|-----------------------| +| GET | `/hello` | Returns a JSON greeting message | `{"message": "hello"}` | 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..40cd178 --- /dev/null +++ b/app/main.py @@ -0,0 +1,20 @@ +"""FastAPI application entry point. + +Initialises the FastAPI app and defines the GET /hello endpoint. +""" + +from __future__ import annotations + +from fastapi import FastAPI + +app = FastAPI( + title="Hello API", + description="A minimal API server with a /hello endpoint.", + version="0.1.0", +) + + +@app.get("/hello", tags=["hello"]) +async def hello() -> dict: + """Return a simple JSON greeting.""" + return {"message": "hello"} diff --git a/conftest.py b/conftest.py index 2cb6af4..e21aa40 100644 --- a/conftest.py +++ b/conftest.py @@ -7,6 +7,11 @@ 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..6011f7d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.9" + +services: + api: + build: . + ports: + - "8000:8000" + volumes: + - .:/app + environment: + - PYTHONUNBUFFERED=1 + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload 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_hello.py b/tests/test_hello.py new file mode 100644 index 0000000..9e89c38 --- /dev/null +++ b/tests/test_hello.py @@ -0,0 +1,42 @@ +"""Tests for the GET /hello endpoint. + +Verifies that the endpoint returns HTTP 200 and the expected JSON body. +""" + +from __future__ import annotations + +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_get_hello_status_code() -> None: + """GET /hello should return HTTP 200.""" + response = client.get("/hello") + assert response.status_code == 200 + + +def test_get_hello_json_body() -> None: + """GET /hello should return JSON {'message': 'hello'}.""" + response = client.get("/hello") + assert response.json() == {"message": "hello"} + + +def test_get_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_post_hello_not_allowed() -> None: + """POST /hello should return HTTP 405 Method Not Allowed.""" + response = client.post("/hello") + assert response.status_code == 405 + + +def test_nonexistent_endpoint_returns_404() -> None: + """Requesting a non-existent path should return HTTP 404.""" + response = client.get("/nonexistent") + assert response.status_code == 404