From f5789c8660228218bc379e1a34dabeefdba39fa1 Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 00:34:10 +0000 Subject: [PATCH 1/3] feat: Create FastAPI App and /hello Endpoint Run: 6f0e7206-b54c-4907-952d-86a2dd78ee82 Task: c8bea064-4f6e-49dd-814c-d990b366f8de Agent: builder --- Dockerfile | 12 +++++++++ README.md | 62 ++++++++++++++++++++------------------------- RUNNING.md | 39 ++++++++++++++++++---------- app/__init__.py | 0 app/main.py | 20 +++++++++++++++ docker-compose.yml | 9 +++++++ requirements.txt | 5 ++-- tests/test_hello.py | 45 ++++++++++++++++++++++++++++++++ 8 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 Dockerfile create mode 100644 app/__init__.py create mode 100644 app/main.py create mode 100644 docker-compose.yml create mode 100644 tests/test_hello.py 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/README.md b/README.md index 1cbd65b..8f733e8 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,45 @@ -# Phalanx Showcase +# Hello World FastAPI Application -Apps and features built entirely by Phalanx — no human wrote the code. +A minimal FastAPI application with a single `/hello` endpoint. -> Each directory is a standalone project generated by `/phalanx build` from a single prompt. +## Setup ---- +### Local Development -## How it works - -1. Run `/phalanx build ""` in Slack -2. Phalanx plans, builds, reviews, tests, and opens a PR against this repo -3. You approve the merge -4. The generated code lands here - ---- - -## Projects - -| Project | Prompt | Status | -|---------|--------|--------| -| `hello-world/` | `Add a GET /hello endpoint that returns Hello World!` | In progress | +```bash +pip install -r requirements.txt +``` ---- +### Running the Application -## Running a project locally +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` -Each project includes its own `README.md` with setup instructions. Generally: +### Running with Docker ```bash -cd -# follow the project README +docker compose up --build ``` ---- +The API will be available at `http://localhost:8000`. -## Adding this repo as a build target +## Endpoints -In your Phalanx project config (`configs/team.yaml`), set: +| Method | Path | Description | +|--------|----------|--------------------------------------| +| GET | `/hello` | Returns `{"message": "Hello, World!"}` | -```yaml -showcase_repo: https://github.com/usephalanx/showcase -``` +## Running Tests -The Release agent will open PRs against this repo when a run completes. +```bash +pytest tests/ -v +``` ---- +### With Coverage -## Links +```bash +pytest tests/ --cov=app --cov-report=term-missing +``` -- Main product: [usephalanx/phalanx](https://github.com/usephalanx/phalanx) -- Website: [usephalanx.com](https://usephalanx.com) -- X: [@usephalanx](https://x.com/usephalanx) +Coverage threshold: **70%** diff --git a/RUNNING.md b/RUNNING.md index 77896cf..aadc8c0 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,33 +1,44 @@ -# Running the Todo API +# Running the Application -## Prerequisites +## TEAM_BRIEF +stack: Python/FastAPI +test_runner: pytest tests/ +lint_tool: ruff check . +coverage_tool: pytest-cov +coverage_threshold: 70 +coverage_applies: true -- Python 3.10 or later +## Docker-Based Instructions -## Install dependencies +### Build and Run ```bash -pip install fastapi uvicorn pydantic +docker compose up --build ``` -For running the test suite you will also need: +The API will be available at `http://localhost:8000`. + +### Run Tests ```bash -pip install httpx pytest +pip install -r requirements.txt +pytest tests/ -v ``` -## Start the server +### Run Tests with Coverage ```bash -uvicorn main:app --reload --host 0.0.0.0 --port 8000 +pytest tests/ --cov=app --cov-report=term-missing ``` -The API will be available at . +### Verify the Endpoint -Interactive docs are served at . +```bash +curl http://localhost:8000/hello +``` -## Run the tests +Expected response: -```bash -pytest tests/ +```json +{"message": "Hello, World!"} ``` 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..cd1a819 --- /dev/null +++ b/app/main.py @@ -0,0 +1,20 @@ +"""FastAPI application entry point. + +Creates the FastAPI app instance and defines the /hello endpoint. +""" + +from __future__ import annotations + +from fastapi import FastAPI + +app = FastAPI( + title="Hello World API", + description="A simple FastAPI application with a /hello endpoint.", + version="1.0.0", +) + + +@app.get("/hello", tags=["hello"]) +async def hello() -> dict: + """Return a simple hello world JSON message.""" + return {"message": "Hello, World!"} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c8167a9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,9 @@ +version: "3.9" + +services: + web: + build: . + ports: + - "8000:8000" + environment: + - PYTHONUNBUFFERED=1 diff --git a/requirements.txt b/requirements.txt index 5a6ebf7..69d8a3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,5 @@ fastapi>=0.100.0 -uvicorn>=0.23.0 -pydantic>=2.0.0 +uvicorn[standard]>=0.23.0 pytest>=7.0.0 -pytest-timeout>=2.1.0 httpx>=0.24.0 +pytest-cov>=4.0.0 diff --git a/tests/test_hello.py b/tests/test_hello.py new file mode 100644 index 0000000..abd1011 --- /dev/null +++ b/tests/test_hello.py @@ -0,0 +1,45 @@ +"""Tests for the /hello endpoint.""" + +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 for the FastAPI app.""" + return TestClient(app) + + +def test_hello_endpoint_returns_200(client: TestClient) -> None: + """GET /hello should return HTTP 200.""" + response = client.get("/hello") + assert response.status_code == 200 + + +def test_hello_endpoint_returns_correct_json(client: TestClient) -> None: + """GET /hello should 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 should return application/json content type.""" + response = client.get("/hello") + assert "application/json" in response.headers["content-type"] + + +def test_hello_endpoint_with_query_params(client: TestClient) -> None: + """GET /hello with unexpected query params still returns correct response.""" + response = client.get("/hello", params={"foo": "bar", "baz": "123"}) + assert response.status_code == 200 + assert response.json() == {"message": "Hello, World!"} + + +def test_nonexistent_endpoint_returns_404(client: TestClient) -> None: + """Requesting a non-existent endpoint should return 404.""" + response = client.get("/nonexistent") + assert response.status_code == 404 From f6cf5c7cfb1aef3ad1e38e34d1c5e94d3db1800d Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 00:34:40 +0000 Subject: [PATCH 2/3] feat: Add requirements.txt for FastAPI App Run: 6f0e7206-b54c-4907-952d-86a2dd78ee82 Task: 293c856b-0b03-40dc-9469-219fbab7a96f Agent: builder --- README.md | 18 ++++------------- RUNNING.md | 27 +++++++++++++++----------- app/__init__.py | 1 + conftest.py | 9 +++++++++ pytest.ini | 3 +++ tests/__init__.py | 1 + tests/test_hello.py | 47 ++++++++++++++++++++------------------------- 7 files changed, 55 insertions(+), 51 deletions(-) create mode 100644 pytest.ini diff --git a/README.md b/README.md index 8f733e8..5a5be5d 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,25 @@ # Hello World FastAPI Application -A minimal FastAPI application with a single `/hello` endpoint. +A minimal FastAPI application with a `/hello` endpoint. ## Setup -### Local Development - ```bash pip install -r requirements.txt ``` -### Running the Application +## Running the Application ```bash uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -### Running with Docker +Or with Docker: ```bash docker compose up --build ``` -The API will be available at `http://localhost:8000`. - -## Endpoints - -| Method | Path | Description | -|--------|----------|--------------------------------------| -| GET | `/hello` | Returns `{"message": "Hello, World!"}` | - ## Running Tests ```bash @@ -42,4 +32,4 @@ pytest tests/ -v pytest tests/ --cov=app --cov-report=term-missing ``` -Coverage threshold: **70%** +Coverage threshold target: **70%** diff --git a/RUNNING.md b/RUNNING.md index aadc8c0..f45d156 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -1,4 +1,4 @@ -# Running the Application +# Running Instructions ## TEAM_BRIEF stack: Python/FastAPI @@ -8,20 +8,23 @@ coverage_tool: pytest-cov coverage_threshold: 70 coverage_applies: true -## Docker-Based Instructions +## Local Development -### Build and Run +### Install Dependencies ```bash -docker compose up --build +pip install -r requirements.txt ``` -The API will be available at `http://localhost:8000`. +### Run the Application + +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` ### Run Tests ```bash -pip install -r requirements.txt pytest tests/ -v ``` @@ -31,14 +34,16 @@ pytest tests/ -v pytest tests/ --cov=app --cov-report=term-missing ``` -### Verify the Endpoint +## Docker + +### Build and Run ```bash -curl http://localhost:8000/hello +docker compose up --build ``` -Expected response: +### Run Tests in Docker -```json -{"message": "Hello, World!"} +```bash +docker compose run --rm web pytest tests/ --cov=app --cov-report=term-missing ``` diff --git a/app/__init__.py b/app/__init__.py index e69de29..df935f3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -0,0 +1 @@ +"""FastAPI hello-world application package.""" diff --git a/conftest.py b/conftest.py index 2cb6af4..fa6e700 100644 --- a/conftest.py +++ b/conftest.py @@ -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.""" @@ -20,3 +23,9 @@ def pytest_addoption(parser): except ValueError: # Already registered (pytest-timeout is installed) pass + + +@pytest.fixture +def anyio_backend() -> str: + """Select the asyncio backend for anyio-based tests.""" + return "asyncio" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..78c5011 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +asyncio_mode = auto +testpaths = tests diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..3c6df15 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test suite for the FastAPI application.""" diff --git a/tests/test_hello.py b/tests/test_hello.py index abd1011..30f7f5a 100644 --- a/tests/test_hello.py +++ b/tests/test_hello.py @@ -1,45 +1,40 @@ -"""Tests for the /hello endpoint.""" +"""Tests for the /hello endpoint in app.main.""" from __future__ import annotations import pytest -from fastapi.testclient import TestClient +from httpx import ASGITransport, AsyncClient from app.main import app -@pytest.fixture() -def client() -> TestClient: - """Return a TestClient instance for the FastAPI app.""" - return TestClient(app) +@pytest.mark.anyio +async def test_hello_endpoint() -> None: + """GET /hello should return 200 with the expected JSON body.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/hello") - -def test_hello_endpoint_returns_200(client: TestClient) -> None: - """GET /hello should return HTTP 200.""" - response = client.get("/hello") assert response.status_code == 200 - - -def test_hello_endpoint_returns_correct_json(client: TestClient) -> None: - """GET /hello should 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 should return application/json content type.""" - response = client.get("/hello") - assert "application/json" in response.headers["content-type"] - - -def test_hello_endpoint_with_query_params(client: TestClient) -> None: +@pytest.mark.anyio +async def test_hello_endpoint_with_query_params() -> None: """GET /hello with unexpected query params still returns correct response.""" - response = client.get("/hello", params={"foo": "bar", "baz": "123"}) + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/hello", params={"foo": "bar"}) + assert response.status_code == 200 assert response.json() == {"message": "Hello, World!"} -def test_nonexistent_endpoint_returns_404(client: TestClient) -> None: - """Requesting a non-existent endpoint should return 404.""" - response = client.get("/nonexistent") +@pytest.mark.anyio +async def test_nonexistent_endpoint_returns_404() -> None: + """Requesting a non-existent path should return 404.""" + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as client: + response = await client.get("/nonexistent") + assert response.status_code == 404 From 838b5cc6256cfbc8a1458de6ceb26ce51f90ca2c Mon Sep 17 00:00:00 2001 From: FORGE Date: Sat, 11 Apr 2026 00:35:42 +0000 Subject: [PATCH 3/3] feat: Write Complete Test Suite for FastAPI App Run: 6f0e7206-b54c-4907-952d-86a2dd78ee82 Task: f6d5d50a-4d12-4bef-8ce2-345bda85101b Agent: builder --- README.md | 20 ++++--- RUNNING.md | 21 +++----- docker-compose.yml | 2 +- pytest.ini | 1 - requirements.txt | 12 +++-- tests/__init__.py | 2 +- tests/test_hello.py | 125 +++++++++++++++++++++++++++++++++++++------- 7 files changed, 132 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 5a5be5d..99c1682 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Hello World FastAPI Application +# Hello World FastAPI -A minimal FastAPI application with a `/hello` endpoint. +A minimal FastAPI application with a `/hello` endpoint and full test coverage. ## Setup @@ -8,28 +8,26 @@ A minimal FastAPI application with a `/hello` endpoint. pip install -r requirements.txt ``` -## Running the Application +## Run the app ```bash uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -Or with Docker: +## Run tests ```bash -docker compose up --build +pytest tests/ -v ``` -## Running Tests +## Run tests with coverage ```bash -pytest tests/ -v +pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70 ``` -### With Coverage +## Docker ```bash -pytest tests/ --cov=app --cov-report=term-missing +docker compose up --build ``` - -Coverage threshold target: **70%** diff --git a/RUNNING.md b/RUNNING.md index f45d156..1aa727f 100644 --- a/RUNNING.md +++ b/RUNNING.md @@ -2,7 +2,7 @@ ## TEAM_BRIEF stack: Python/FastAPI -test_runner: pytest tests/ +test_runner: pytest tests/ -v lint_tool: ruff check . coverage_tool: pytest-cov coverage_threshold: 70 @@ -10,40 +10,33 @@ coverage_applies: true ## Local Development -### Install Dependencies - ```bash pip install -r requirements.txt -``` - -### Run the Application - -```bash uvicorn app.main:app --host 0.0.0.0 --port 8000 ``` -### Run Tests +## Running Tests ```bash pytest tests/ -v ``` -### Run Tests with Coverage +## Running Tests with Coverage ```bash -pytest tests/ --cov=app --cov-report=term-missing +pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70 ``` ## Docker -### Build and Run +### Build and run ```bash docker compose up --build ``` -### Run Tests in Docker +### Run tests in container ```bash -docker compose run --rm web pytest tests/ --cov=app --cov-report=term-missing +docker compose run --rm api pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70 ``` diff --git a/docker-compose.yml b/docker-compose.yml index c8167a9..977b558 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: - web: + api: build: . ports: - "8000:8000" diff --git a/pytest.ini b/pytest.ini index 78c5011..2f4c80e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,2 @@ [pytest] asyncio_mode = auto -testpaths = tests diff --git a/requirements.txt b/requirements.txt index 69d8a3f..054057d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,7 @@ -fastapi>=0.100.0 -uvicorn[standard]>=0.23.0 -pytest>=7.0.0 -httpx>=0.24.0 -pytest-cov>=4.0.0 +fastapi +uvicorn[standard] +pytest +httpx +pytest-cov +anyio +pytest-asyncio diff --git a/tests/__init__.py b/tests/__init__.py index 3c6df15..fe5fa51 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1 +1 @@ -"""Test suite for the FastAPI application.""" +"""Test package for the FastAPI application.""" diff --git a/tests/test_hello.py b/tests/test_hello.py index 30f7f5a..6256aad 100644 --- a/tests/test_hello.py +++ b/tests/test_hello.py @@ -1,4 +1,11 @@ -"""Tests for the /hello endpoint in app.main.""" +"""Tests for the /hello endpoint and FastAPI app basics. + +Covers: +- GET /hello returns 200 with correct JSON body +- GET /hello with unexpected query params still succeeds +- Non-existent routes return 404 +- App metadata is configured correctly +""" from __future__ import annotations @@ -8,33 +15,115 @@ from app.main import app -@pytest.mark.anyio -async def test_hello_endpoint() -> None: - """GET /hello should return 200 with the expected JSON body.""" +@pytest.fixture +def client() -> AsyncClient: + """Create an async HTTP client bound to the FastAPI app.""" transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/hello") + return AsyncClient(transport=transport, base_url="http://testserver") + +@pytest.mark.anyio +async def test_hello_endpoint_returns_200(client: AsyncClient) -> None: + """GET /hello should return HTTP 200.""" + response = await client.get("/hello") assert response.status_code == 200 - assert response.json() == {"message": "Hello, World!"} @pytest.mark.anyio -async def test_hello_endpoint_with_query_params() -> None: - """GET /hello with unexpected query params still returns correct response.""" - transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/hello", params={"foo": "bar"}) +async def test_hello_endpoint_returns_correct_json(client: AsyncClient) -> None: + """GET /hello should return the expected JSON body.""" + response = await client.get("/hello") + data = response.json() + assert data == {"message": "Hello, World!"} + +@pytest.mark.anyio +async def test_hello_endpoint_content_type(client: AsyncClient) -> None: + """GET /hello should return application/json content type.""" + response = await client.get("/hello") + assert "application/json" in response.headers["content-type"] + + +@pytest.mark.anyio +async def test_hello_endpoint_with_query_params(client: AsyncClient) -> None: + """GET /hello with unexpected query params should still return 200 and correct JSON.""" + response = await client.get("/hello", params={"foo": "bar", "baz": "123"}) assert response.status_code == 200 assert response.json() == {"message": "Hello, World!"} @pytest.mark.anyio -async def test_nonexistent_endpoint_returns_404() -> None: - """Requesting a non-existent path should return 404.""" - transport = ASGITransport(app=app) - async with AsyncClient(transport=transport, base_url="http://test") as client: - response = await client.get("/nonexistent") - +async def test_nonexistent_route_returns_404(client: AsyncClient) -> None: + """GET on a non-existent route should return HTTP 404.""" + response = await client.get("/nonexistent") assert response.status_code == 404 + + +@pytest.mark.anyio +async def test_hello_post_method_not_allowed(client: AsyncClient) -> None: + """POST /hello should return HTTP 405 Method Not Allowed.""" + response = await client.post("/hello") + assert response.status_code == 405 + + +@pytest.mark.anyio +async def test_hello_put_method_not_allowed(client: AsyncClient) -> None: + """PUT /hello should return HTTP 405 Method Not Allowed.""" + response = await client.put("/hello") + assert response.status_code == 405 + + +@pytest.mark.anyio +async def test_hello_delete_method_not_allowed(client: AsyncClient) -> None: + """DELETE /hello should return HTTP 405 Method Not Allowed.""" + response = await client.delete("/hello") + assert response.status_code == 405 + + +def test_app_title() -> None: + """The app title should be set correctly.""" + assert app.title == "Hello World API" + + +def test_app_version() -> None: + """The app version should be set correctly.""" + assert app.version == "1.0.0" + + +def test_app_description() -> None: + """The app description should be set correctly.""" + assert app.description == "A simple FastAPI application with a /hello endpoint." + + +@pytest.mark.anyio +async def test_hello_response_message_key_exists(client: AsyncClient) -> None: + """GET /hello response must contain the 'message' key.""" + response = await client.get("/hello") + data = response.json() + assert "message" in data + + +@pytest.mark.anyio +async def test_hello_response_message_value_type(client: AsyncClient) -> None: + """GET /hello 'message' value must be a string.""" + response = await client.get("/hello") + data = response.json() + assert isinstance(data["message"], str) + + +@pytest.mark.anyio +async def test_hello_response_has_single_key(client: AsyncClient) -> None: + """GET /hello response should contain exactly one key.""" + response = await client.get("/hello") + data = response.json() + assert len(data) == 1 + + +@pytest.mark.anyio +async def test_openapi_schema_available(client: AsyncClient) -> None: + """The OpenAPI schema endpoint should be accessible.""" + response = await client.get("/openapi.json") + assert response.status_code == 200 + schema = response.json() + assert "paths" in schema + assert "/hello" in schema["paths"]