Tech stack
| Layer | Technology | Role |
|---|---|---|
| Web framework | FastAPI | Async-capable Python router, automatic request validation via Pydantic, built-in OpenAPI docs at /docs |
| ORM | SQLAlchemy | Maps Python classes to SQL tables; used directly in route handlers with no service layer |
| Database | SQLite | Zero-configuration embedded database, no separate server process. File: risk_monitor.db |
| Templates | Jinja2 | Server-side HTML rendering; pairs naturally with HTMX’s partial-swap model |
| UI interactivity | HTMX | Interactive UI via HTML attributes — filtering, sorting, pagination, and risk evaluation all work without writing JavaScript |
| Container | Docker | Single-container deployment using python:3.12-slim, port 8000, uvicorn entrypoint |
Request flow
Every request follows the same path:/businesses), FastAPI renders and returns a full HTML page. For an HTMX-triggered interaction (e.g. filtering the business list), FastAPI detects the HX-Request header and returns only the HTML fragment needed to update part of the page — no full reload required.
HTMX partials pattern
HTMX drives all dynamic interactions in the UI:- Filtering — submitting the search form sends a
GETwith query parameters; the response swaps in updated table rows - Sorting — clicking a column header triggers the same route with
?sort=and?order=parameters - Pagination — page links include
?page=and update only the table region - Risk evaluation —
POST /businesses/{id}/evaluatereturns apartials/risk_display.htmlfragment that replaces the score display in place
HX-Request header is absent.
Design decisions
Single routes file — with five business endpoints plus a test runner, splitting into multiple router modules would add indirection without value. Each route has a clear docstring andapp/routes.py stays under 250 lines.
No service layer — routes query SQLAlchemy directly. Two models and one evaluation function don’t justify an extra abstraction layer. If the app grows, extracting services is a straightforward refactor.
Factors stored as JSON text — SQLite has no native JSON column type. The factors column stores json.dumps(...) in a Text column and is parsed on read with json.loads(). In a production PostgreSQL setup this would be a native jsonb column.
The simulated risk engine uses deterministic industry and country modifiers plus controlled randomness. This makes results testable (statistical tests verify high-risk inputs trend higher) while still producing varied scores on each evaluation run.
Container
The app ships as a single Docker container:main.py creates all database tables (Base.metadata.create_all) and seeds 50 sample businesses if the database is empty.
Explore further
Risk engine
How the simulated scoring algorithm computes risk scores from industry and country inputs
Data model
The two SQLAlchemy models, their columns, relationships, and the JSON text pattern for factors