The problem
Exchange rates in Uzbekistan come in two flavours: the official central-bank rate, and what you are actually offered at a bank window — and the gap between them is the whole point. Nobody publishes both in one place with history, and the apps that come close want an account, show ads, and stop working when their API key expires.
I wanted a rate tracker I could still run in five years without touching it.
The constraint that shaped everything
Zero dependencies. requirements.txt is empty on purpose. HTTP is
urllib, storage is sqlite3, the Telegram client is written
against the raw Bot API, and charts are drawn by emitting SVG and rasterising it by hand.
Nothing outside the Python standard library.
This started as an exercise and turned into the most defensible decision in the project. A dependency is a promise someone else has to keep. The failure mode I was designing against was not a bug — it was coming back after a year to a project that will not install.
What it cost
- Charts. No matplotlib means writing the axis scaling, tick selection and label placement myself, then a minimal PNG encoder to hand Telegram an image it accepts. That is a weekend I would not have spent otherwise.
- The bot. No aiogram means implementing longpolling, offsets, retries and update routing by hand. Roughly 300 lines that a library gives you free.
- Async. Without a framework, concurrency is not free either, so collection is sequential. For a job that runs a few times a day against a handful of sources, that is the right amount of engineering.
What it bought
git clone && python3 run.pyworks. No virtualenv, no install step, no lockfile drift, nothing to audit.- Every line in the repository is a line I can explain. When a rate looks wrong, the distance from symptom to cause is short.
- The Telegram client and the chart renderer are useful on their own, because they carry no framework with them.
Architecture
Screens and states
CLI. The same core the bot uses. If it works here, it works there.
Charts. Axis scaling, ticks and the PNG encoder are all in-repo.
Alerts. Thresholds in SQLite, checked after each collection pass.
Partial failure. A broken scraper is normal operation, not an outage.
Results and lessons
cpWhat I would tell myself at the start
- Isolate the part that will break. Bank websites change markup; that is not a possibility, it is a schedule. Each source is one file behind one interface, so a break is contained and a fix is obvious.
- Write the constraint down in the repository. The rule lives in
CLAUDE.md, because six months later a helpful person — often me — reaches forrequestswithout knowing the empty file was a decision. - “No dependencies” is not the same as “no abstractions”. The standard library gives you less, so the seams have to be drawn deliberately.