A coding kata taken to a production-grade, documented, deployed service.
RandomGen is a small Flask REST API that draws random numbers from a configurable discrete distribution and scores the sample against it with a Chi-Square goodness-of-fit test.
🌐 Live demo: https://randomgen-llyc.onrender.com/ — try the interactive UI or the API directly (free Render instance; the first request after idle may cold-start for ~30–60s).
- Two interchangeable generators —
/api/v1and/api/v2. - Stateless sampling from a built-in or per-request distribution.
- A Chi-Square goodness-of-fit report on every response.
- An interactive browser UI — presets, weight sliders, light/dark, CSV export.
- An OpenAPI 3.1 contract with a ReDoc reference at
/docs. /infoand/healthendpoints, on a hardened, non-root Docker image on gunicorn.
Prerequisites: Docker.
docker pull braboj/randomgen:latest
docker run -p 5000:5000 braboj/randomgen:latestOpen http://localhost:5000 for the interactive home page — a small UI to pick a generator, distribution, and sample size and see the Chi-Square verdict with an expected-vs-observed histogram (see the UI snapshots for a preview), or browse the interactive API reference at http://localhost:5000/docs. Or call the API directly to generate 100 numbers:
curl "http://localhost:5000/api/v1/randomgen?numbers=100"Generate numbers from the built-in distribution:
curl "http://localhost:5000/api/v1/randomgen?numbers=1000"Expected output (truncated):
{
"numbers": [1, 0, 1, 2, 1, 0, 1, 1, -1, 1],
"quality": {
"chi_square_test": {
"is_null": true,
"chi_square": 4.97,
"p_value": 0.17,
"df": 4
},
"expected_histogram": {"-1": 0.01, "0": 0.3, "1": 0.58, "2": 0.1, "3": 0.01},
"observed_histogram": {"-1": 0.01, "0": 0.29, "1": 0.59, "2": 0.1, "3": 0.01}
}
}Override the distribution per request with a single dist parameter of
comma-separated value:probability pairs (preferred — each value is bound to
its own weight, and they must sum to 1):
curl "http://localhost:5000/api/v1/randomgen?numbers=1000&dist=1:0.2,2:0.2,3:0.6"The repeated value / probability parameters still work (they must be the
same length and sum to 1):
curl "http://localhost:5000/api/v1/randomgen?numbers=1000&value=1&value=2&value=3&probability=0.2&probability=0.2&probability=0.6"Invalid input — a non-integer numbers, mismatched lengths, or weights that
don't sum to 1 — returns 400 with a JSON {"error": "..."} body.
The repo ships a render.yaml blueprint, so you can run a
zero-cost demo on Render from the published image:
- In the Render dashboard choose New → Blueprint and connect this repository (or use the button above).
- Render provisions a free web service that runs the published
braboj/randomgen:latestimage with a/healthcheck. It injects$PORT; the image binds it automatically (gunicorn ... 0.0.0.0:${PORT:-5000}), so no extra configuration is needed. - Once live, the service is reachable at the URL Render assigns — this project's demo runs at https://randomgen-llyc.onrender.com/.
Releases redeploy automatically: the image workflow POSTs a Render Deploy Hook after pushing a new image (see PLAYBOOK section 5).
Note: free instances spin down after ~15 minutes of inactivity and cold-start (~30–60s) on the next request — expected for a zero-cost demo.
src/randomgen/ # application package — app factory, service, OpenAPI contract
domain/ # framework-free core: generators, histogram, Chi-Square, errors
blueprints/ # web + versioned-API route blueprints
templates/, static/ # home-page UI (Jinja + CSS/JS)
tests/ # pytest suite — unit, integration, e2e
scripts/ # demo, plotting, and API client helpers
docs/ # arc42 architecture, ADRs, history, assets
pyproject.toml # PEP 621 metadata, dependencies, tool config
Dockerfile # non-root, gunicorn, digest-pinned base image
render.yaml # Render free-tier deploy blueprint
gunicorn.conf.py # gunicorn runtime config (bind + workers)
.github/workflows/ # CI (gated jobs), CodeQL (SAST), CD (release + publish + deploy)
Supported Python: 3.14+.
# Clone
git clone https://github.com/braboj/randomgen.git
cd randomgen
# Install the project with the developer toolchain (ruff, mypy, pytest)
pip install -e ".[dev]"
# Lint, type-check, and run the fast test gate (unit + integration)
ruff check . && ruff format --check . && mypy
pytest --cov=randomgen --cov-fail-under=85
# End-to-end tier: real container (Testcontainers on a Podman or Docker
# backend) + a Playwright browser test. One-time setup, then run:
pip install -e ".[e2e]" && playwright install chromium
pytest -m e2e
# Run the service locally (Flask dev server, hot reload)
flask --app "randomgen.app:create_app" run # http://127.0.0.1:5000The service is stateless: per-request behavior is set with query parameters (see Usage), while deployment and limits are set by the keys below.
| Key | Where | Default | Description |
|---|---|---|---|
RANDOMGEN_LOG_LEVEL |
env var | INFO |
Application log level (one log line per request). |
WEB_CONCURRENCY |
env var | 2 |
gunicorn worker count; see gunicorn.conf.py. |
PORT |
env var | 5000 |
Listen port; gunicorn binds $PORT (the Flask dev server uses 5000). |
MAX_NUMBERS |
service.py |
10000 |
Upper bound on numbers. |
| Built-in distribution | service.py |
[-1,0,1,2,3] / [0.01,0.3,0.58,0.1,0.01] |
DEFAULT_NUMBERS / DEFAULT_PROBABILITIES. |
Contributions are welcome. Set up your environment from
Development above, follow the project conventions and session
workflow in CLAUDE.md, and open a PR — one concern per PR,
conventional commits, and green CI before merge (main is branch-protected).
- For the full REST contract, see
src/randomgen/openapi.yaml(rendered at/docs). - For the architecture, see the arc42 documentation.
- For how it was built — from coding kata to deployed service — see docs/history/.
- To contribute, see Contributing above.
- To leave feedback, open a discussion.