Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .coverage
Binary file not shown.
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
20 changes: 20 additions & 0 deletions QA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
app_type: web-api
coverage_applies: true
coverage_source: app
coverage_threshold: 70
coverage_tool: pytest-cov
install_steps:
- pip install -r /tmp/forge-repos/hello-world-fastapi-v2-0006d299/requirements.txt
lint_tool: ruff check .
notes: Verify that all tests pass, linting is clean, and test coverage for the app
package is at least 70%.
stack: Python/FastAPI
test_files:
- tests/__init__.py
- tests/conftest.py
- tests/test_main.py
- tests/test_models.py
- tests/test_storage.py
- tests/test_todos.py
test_runner: pytest tests/ -v
workspace: /tmp/forge-repos/hello-world-fastapi-v2-0006d299
48 changes: 36 additions & 12 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,57 @@
# 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 (optional)

## Install dependencies
## Local Setup

```bash
pip install fastapi uvicorn pydantic
pip install -r requirements.txt
```

For running the test suite you will also need:
## Run the Application

### Using uvicorn directly

```bash
pip install httpx pytest
# Run the app/main.py entry point
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```

## Start the server
### Using Docker

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
docker compose up --build
```

The API will be available at <http://localhost:8000>.
The application will be available at http://localhost:8000

## Key Endpoints

| Method | Path | Description |
|--------|-----------|-------------------------|
| GET | / | Root - Hello World |
| GET | /health | Health check |
| GET | /hello | Hello, World! greeting |

Interactive docs are served at <http://localhost:8000/docs>.
## Run Tests

```bash
pytest tests/ -v
```

## Run the tests
## Run Tests with Coverage

```bash
pytest tests/
pytest tests/ --cov=app --cov-report=term-missing
```
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""App package for the FastAPI application."""
32 changes: 32 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""FastAPI application entry point.

Creates the FastAPI app instance and defines the /hello, /health, and / endpoints.
"""

from __future__ import annotations

from fastapi import FastAPI

app = FastAPI(
title="Hello World API",
description="A simple Hello World FastAPI application.",
version="1.0.0",
)


@app.get("/", tags=["root"])
async def root() -> dict:
"""Return a welcome message at the API root."""
return {"message": "Hello World"}


@app.get("/health", tags=["health"])
async def health() -> dict:
"""Return the health status of the application."""
return {"status": "ok"}


@app.get("/hello", tags=["hello"])
async def hello() -> dict:
"""Return a Hello, World! greeting message."""
return {"message": "Hello, World!"}
9 changes: 9 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@

Registers the --timeout option so that pytest does not fail with
'unrecognized arguments' when pytest-timeout is not installed.
Configures anyio backend for async tests.
"""

import sys
from pathlib import Path

import pytest


def pytest_addoption(parser):
"""Register --timeout so pytest doesn't choke when the plugin is absent."""
Expand All @@ -20,3 +23,9 @@ def pytest_addoption(parser):
except ValueError:
# Already registered (pytest-timeout is installed)
pass


@pytest.fixture
def anyio_backend():
"""Select asyncio as the anyio backend for async tests."""
return "asyncio"
29 changes: 29 additions & 0 deletions coverage.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" ?>
<coverage version="7.13.5" timestamp="1775879786790" lines-valid="6" lines-covered="6" line-rate="1" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.13.5 -->
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
<sources>
<source>/tmp/forge-repos/hello-world-fastapi-v2-0006d299/app</source>
</sources>
<packages>
<package name="." line-rate="1" branch-rate="0" complexity="0">
<classes>
<class name="__init__.py" filename="__init__.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
<lines/>
</class>
<class name="main.py" filename="main.py" complexity="0" line-rate="1" branch-rate="0">
<methods/>
<lines>
<line number="6" hits="1"/>
<line number="8" hits="1"/>
<line number="10" hits="1"/>
<line number="17" hits="1"/>
<line number="18" hits="1"/>
<line number="20" hits="1"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: "3.9"

services:
app:
build: .
ports:
- "8000:8000"
environment:
- PYTHONUNBUFFERED=1
9 changes: 8 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""FastAPI application entry point.

Creates the FastAPI app and mounts the Todo CRUD router.
Provides /health and / root endpoints.
"""

from __future__ import annotations
Expand All @@ -21,4 +22,10 @@
@app.get("/", tags=["root"])
async def root() -> dict:
"""Return a welcome message at the API root."""
return {"message": "Welcome to the Todo API"}
return {"message": "Hello World"}


@app.get("/health", tags=["health"])
async def health() -> dict:
"""Return the health status of the application."""
return {"status": "ok"}
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[pytest]
asyncio_mode = auto
11 changes: 5 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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
uvicorn[standard]
pytest
httpx
pytest-cov
1 change: 1 addition & 0 deletions test-results.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><testsuites name="pytest tests"><testsuite name="pytest" errors="0" failures="0" skipped="0" tests="47" time="3.768" timestamp="2026-04-11T03:56:23.025919+00:00" hostname="7635814972b0"><testcase classname="tests.test_main" name="test_hello_returns_200" time="0.018" /><testcase classname="tests.test_main" name="test_hello_returns_correct_json" time="0.006" /><testcase classname="tests.test_main" name="test_hello_content_type_is_json" time="0.005" /><testcase classname="tests.test_main" name="test_hello_post_method_not_allowed" time="0.004" /><testcase classname="tests.test_main" name="test_hello_put_method_not_allowed" time="0.004" /><testcase classname="tests.test_main" name="test_hello_delete_method_not_allowed" time="0.004" /><testcase classname="tests.test_main" name="test_hello_patch_method_not_allowed" time="0.004" /><testcase classname="tests.test_main" name="test_nonexistent_route_returns_404" time="0.005" /><testcase classname="tests.test_models.TestTodoCreate" name="test_create_with_title_only" time="0.001" /><testcase classname="tests.test_models.TestTodoCreate" name="test_create_with_all_fields" time="0.001" /><testcase classname="tests.test_models.TestTodoCreate" name="test_create_missing_title_raises" time="0.001" /><testcase classname="tests.test_models.TestTodoCreate" name="test_create_empty_title_raises" time="0.001" /><testcase classname="tests.test_models.TestTodoUpdate" name="test_update_all_none_by_default" time="0.001" /><testcase classname="tests.test_models.TestTodoUpdate" name="test_update_partial" time="0.001" /><testcase classname="tests.test_models.TestTodoResponse" name="test_response_roundtrip" time="0.001" /><testcase classname="tests.test_models.TestTodoResponse" name="test_response_description_defaults_to_none" time="0.001" /><testcase classname="tests.test_storage.TestAdd" name="test_add_returns_todo_with_id" time="0.001" /><testcase classname="tests.test_storage.TestAdd" name="test_add_increments_id" time="0.001" /><testcase classname="tests.test_storage.TestAdd" name="test_add_with_description_and_completed" time="0.001" /><testcase classname="tests.test_storage.TestGet" name="test_get_existing" time="0.001" /><testcase classname="tests.test_storage.TestGet" name="test_get_nonexistent_returns_none" time="0.001" /><testcase classname="tests.test_storage.TestGet" name="test_get_returns_copy" time="0.001" /><testcase classname="tests.test_storage.TestGetAll" name="test_get_all_empty" time="0.001" /><testcase classname="tests.test_storage.TestGetAll" name="test_get_all_returns_all" time="0.001" /><testcase classname="tests.test_storage.TestUpdate" name="test_update_title" time="0.001" /><testcase classname="tests.test_storage.TestUpdate" name="test_update_completed" time="0.001" /><testcase classname="tests.test_storage.TestUpdate" name="test_update_nonexistent_returns_none" time="0.001" /><testcase classname="tests.test_storage.TestUpdate" name="test_update_no_fields_is_noop" time="0.001" /><testcase classname="tests.test_storage.TestDelete" name="test_delete_existing" time="0.001" /><testcase classname="tests.test_storage.TestDelete" name="test_delete_nonexistent_returns_false" time="0.001" /><testcase classname="tests.test_storage.TestReset" name="test_reset_clears_all" time="0.001" /><testcase classname="tests.test_todos" name="test_create_todo" time="0.010" /><testcase classname="tests.test_todos" name="test_create_todo_with_description" time="0.005" /><testcase classname="tests.test_todos" name="test_create_todo_with_completed" time="0.005" /><testcase classname="tests.test_todos" name="test_create_todo_missing_title" time="0.005" /><testcase classname="tests.test_todos" name="test_create_todo_empty_title" time="0.006" /><testcase classname="tests.test_todos" name="test_list_todos_empty" time="0.005" /><testcase classname="tests.test_todos" name="test_list_todos" time="0.012" /><testcase classname="tests.test_todos" name="test_get_todo" time="0.009" /><testcase classname="tests.test_todos" name="test_get_todo_not_found" time="0.005" /><testcase classname="tests.test_todos" name="test_update_todo_title" time="0.010" /><testcase classname="tests.test_todos" name="test_update_todo_completed" time="0.009" /><testcase classname="tests.test_todos" name="test_update_todo_description" time="0.016" /><testcase classname="tests.test_todos" name="test_update_todo_not_found" time="0.005" /><testcase classname="tests.test_todos" name="test_delete_todo" time="0.013" /><testcase classname="tests.test_todos" name="test_delete_todo_not_found" time="0.005" /><testcase classname="tests.test_todos" name="test_auto_increment_ids" time="0.009" /></testsuite></testsuites>
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Test package for the FastAPI application."""
Loading