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
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"]
58 changes: 19 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,33 @@
# Phalanx Showcase
# Hello World FastAPI

Apps and features built entirely by Phalanx — no human wrote the code.
A minimal FastAPI application with a `/hello` endpoint and full test coverage.

> Each directory is a standalone project generated by `/phalanx build` from a single prompt.

---

## How it works

1. Run `/phalanx build "<feature description>"` 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 |

---

## Running a project locally

Each project includes its own `README.md` with setup instructions. Generally:
## Setup

```bash
cd <project-name>
# follow the project README
pip install -r requirements.txt
```

---
## Run the app

## Adding this repo as a build target
```bash
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

In your Phalanx project config (`configs/team.yaml`), set:
## Run tests

```yaml
showcase_repo: https://github.com/usephalanx/showcase
```bash
pytest tests/ -v
```

The Release agent will open PRs against this repo when a run completes.
## Run tests with coverage

---
```bash
pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70
```

## Links
## Docker

- Main product: [usephalanx/phalanx](https://github.com/usephalanx/phalanx)
- Website: [usephalanx.com](https://usephalanx.com)
- X: [@usephalanx](https://x.com/usephalanx)
```bash
docker compose up --build
```
37 changes: 23 additions & 14 deletions RUNNING.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
# Running the Todo API
# Running Instructions

## Prerequisites
## TEAM_BRIEF
stack: Python/FastAPI
test_runner: pytest tests/ -v
lint_tool: ruff check .
coverage_tool: pytest-cov
coverage_threshold: 70
coverage_applies: true

- Python 3.10 or later

## Install dependencies
## Local Development

```bash
pip install fastapi uvicorn pydantic
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000
```

For running the test suite you will also need:
## Running Tests

```bash
pip install httpx pytest
pytest tests/ -v
```

## Start the server
## Running Tests with Coverage

```bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70
```

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

### Build and run

Interactive docs are served at <http://localhost:8000/docs>.
```bash
docker compose up --build
```

## Run the tests
### Run tests in container

```bash
pytest tests/
docker compose run --rm api pytest tests/ --cov=app --cov-report=term-missing --cov-fail-under=70
```
1 change: 1 addition & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""FastAPI hello-world application package."""
20 changes: 20 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
@@ -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!"}
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() -> str:
"""Select the asyncio backend for anyio-based tests."""
return "asyncio"
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:
api:
build: .
ports:
- "8000:8000"
environment:
- PYTHONUNBUFFERED=1
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
13 changes: 7 additions & 6 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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
anyio
pytest-asyncio
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."""
129 changes: 129 additions & 0 deletions tests/test_hello.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"""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

import pytest
from httpx import ASGITransport, AsyncClient

from app.main import app


@pytest.fixture
def client() -> AsyncClient:
"""Create an async HTTP client bound to the FastAPI app."""
transport = ASGITransport(app=app)
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


@pytest.mark.anyio
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_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"]