A small FastAPI service that converts an amount using ECB exchange rates from Frankfurter. It prioritizes a truthful failure over an uncertain financial number.
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
./run.shPython 3.10 or newer is required.
The service listens on PORT (default 8080). FX_UPSTREAM_BASE is the
Frankfurter root URL (default https://api.frankfurter.dev); the service calls
its /v1/{date} endpoint and, when failed historical probes are ambiguous, its
/v1/currencies metadata endpoint.
PORT=9000 FX_UPSTREAM_BASE=http://localhost:9001 ./run.shRun the complete test suite with:
./test.shTests use httpx.MockTransport and never contact the configured upstream or the
internet.
GET /tools/convert?amount=250&from=EUR&to=TRY&date=2026-08-28All four query parameters are required. Currency codes are case-insensitive three-letter ASCII codes. On success:
{
"amount": 250,
"from": "EUR",
"to": "TRY",
"rate": 47.1234,
"result": 11780.85,
"rate_date": "2026-08-28",
"asked_date": "2026-08-28",
"source": "ECB via frankfurter.dev"
}asked_date is always the date supplied by the caller. rate_date is always
the publication date read from the validated upstream payload. For an ECB
weekend or holiday, the service uses the most recent safely available earlier
publication, so these fields can differ. It first allows the historical
endpoint to supply a previous rate, then probes at most seven earlier calendar
days. A later publication is never used for a historical request.
Amounts must be finite, greater than zero, and have at most two decimal places.
Conversion uses Decimal, keeps the upstream rate's full precision, and rounds
only the final result to two places using ROUND_HALF_UP. Equal source and
target currencies are rejected.
Only validated successful rate lookups are cached in memory. The key is
(from, to, asked_date), so different amounts reuse a rate while different
dates cannot contaminate each other. The cache lasts for the application
process. A successfully validated provider currency catalog is also cached in
process; no hardcoded currency list is used.
Every failure is non-2xx and has exactly this shape:
{"error": "invalid_amount", "message": "Amount must be greater than zero."}| Status | Code | Meaning |
|---|---|---|
| 404/405/422 | invalid_request |
A required query parameter/date is missing or malformed, or the operation is unavailable. |
| 422 | invalid_amount |
Amount is invalid, non-positive, non-finite, over-precise, or too large to calculate safely. |
| 422 | invalid_currency |
A code has invalid syntax or the upstream does not support it. |
| 422 | same_currency |
Source and target are equal after normalization. |
| 422 | future_date |
The requested date is in the future. |
| 404 | date_out_of_range |
All bounded historical probes were outside available history. |
| 404 | rate_not_available |
Valid historical responses contained no safely usable target rate. |
| 504 | upstream_timeout |
The upstream exceeded the five-second timeout. |
| 502 | upstream_unavailable |
The upstream could not be reached. |
| 502 | upstream_error |
The upstream returned a server or unexpected HTTP error. |
| 502 | invalid_upstream_response |
JSON, base, publication date, rates, or rate value failed validation. |
| 500 | internal_error |
An unexpected failure prevented a safe conversion. |
Upstream failures never become successful zero-value conversions. Frankfurter
v1 uses 404 both for unsupported currencies and unavailable historical dates,
so after a failed bounded search the service consults the configured upstream's
currency catalog. A missing requested code returns invalid_currency; supported
codes with all historical probes missing return date_out_of_range; valid
historical payloads without the target rate return rate_not_available. A fake
upstream that does not implement the catalog may return 404, in which case the
bounded historical classification is preserved. Catalog failures are mapped to
the same upstream error codes listed above.